UNPKG

@avonjs/avonjs

Version:

A fluent Node.js API generator.

384 lines (383 loc) 14.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const joi_1 = __importDefault(require("joi")); const Avon_1 = __importDefault(require("../Avon")); const FieldCollection_1 = __importDefault(require("../Collections/FieldCollection")); const Contracts_1 = require("../Contracts"); const Exceptions_1 = require("../Exceptions"); const Relation_1 = __importDefault(require("./Relation")); const ResourceRelationshipGuesser_1 = require("./ResourceRelationshipGuesser"); class BelongsToMany extends Relation_1.default { /** * The pivot resource instance */ pivotResource; /** * The foreign key of the related model. * The attribute name that holds the parent model key. */ resourceForeignKey; /** * The associated key on the related model. * Defaults to primary key of related model. */ resourceOwnerKey; /** * Indicates fields uses to update pivot table. */ pivotFields = (request) => []; /** * The callback that should be run to pivots table. */ pivotQueryCallback = (request, repository) => this.pivotResource.relatableQuery(request, repository) ?? repository; /** * The callback that should be run to sanitize related resources. */ sanitizeCallback = (request, resources) => resources; constructor(resource, pivot, attribute) { super(resource); this.pivotResource = this.getPivotResource(pivot); this.attribute = attribute ?? resource; this.nullable(true, (value) => !Array.isArray(value) || value.length === 0); } /** * Determine the pivot resource query. */ pivotQueryUsing(pivotQueryCallback) { this.pivotQueryCallback = pivotQueryCallback; return this; } /** * Get the pivot resource. */ getPivotResource(resourceName) { const resource = Avon_1.default.resourceForKey(resourceName); Exceptions_1.RuntimeException.unless(resource, `Invalid pivot:${resourceName} prepared for relation ${this.attribute}`); return resource; } /** * Determine pivot fields. */ pivots(callback) { this.pivotFields = callback; return this; } /** * Get the validation rules for this field. */ getRules(request) { const rules = super.getRules(request); const pivotFields = this.pivotFields(request); const pivotRules = this.pivotResource.prepareRulesForValidator(pivotFields.map((field) => field.getRules(request))); return { ...rules, [this.attribute]: rules[this.attribute].concat(pivotFields.length === 0 ? joi_1.default.array() .items(joi_1.default.string(), joi_1.default.number()) .external(this.existenceRule(request)) : joi_1.default.array().items(joi_1.default.object(pivotRules).append({ id: joi_1.default.alternatives(joi_1.default.string(), joi_1.default.number()).external(this.existenceRule(request)), }))), }; } /** * Get Joi rule to validate resource existence. */ existenceRule(request) { return async (value, { error }) => { if (this.isNullable() && (!Array.isArray(value) || value.length === 0)) { return; } try { const repository = this.relatedResource .resolveRepository(request) .where({ key: this.ownerKeyName(request), operator: Contracts_1.Operator.in, value, }); // to ensure only valid data attached const query = this.relatableQueryCallback.apply(repository, [ request, repository, ]) ?? repository; const resources = await query.all(); if (resources.length !== value.length) { return error('any.custom', { error: new Error('Some of related resources not found'), }); } } catch (err) { return error('any.custom', { error: err }); } }; } /** * Set related model foreign key. */ setResourceForeignKey(resourceForeignKey) { this.resourceForeignKey = resourceForeignKey; return this; } /** * Get attribute that hold the related model key. */ resourceForeignKeyName(request) { return this.resourceForeignKey ?? (0, ResourceRelationshipGuesser_1.guessForeignKey)(request.resource()); } /** * Set the related model owner key. */ setResourceOwnerKey(resourceOwnerKey) { this.resourceOwnerKey = resourceOwnerKey; return this; } /** * Get attribute that hold the related model key. */ resourceOwnerKeyName(request) { return this.resourceOwnerKey ?? request.model().getKeyName(); } /** * Hydrate the given attribute on the model based on the incoming request. */ fillForAction(request, model) { } /** * Hydrate the given attribute on the model based on the incoming request. */ fillAttributeFromRequest(request, requestAttribute) { const defaults = this.resolveDefaultValue(request); const shouldSetDefaults = request.isCreateOrAttachRequest() && Array.isArray(defaults) && defaults.length > 0; if (request.exists(requestAttribute) || shouldSetDefaults) { return async (request, model) => { await request .resource() .authorizeTo(request, Contracts_1.Ability.toggleAttachment, [ this.relatedResource, ]); // first we clear old attachments await this.clearAttachments(request, model); // then fill with new attachments const repository = this.pivotResource.resolveRepository(request); const attachments = await this.prepareAttachments(request, model, requestAttribute); await Promise.all(attachments.map((pivot) => { return repository.store(pivot.setAttribute(this.resourceForeignKeyName(request), model.getAttribute(this.resourceOwnerKeyName(request)))); })); }; } } /** * Detach all related models. */ async clearAttachments(request, resource) { const detaches = await this.pivotResource .resolveRepository(request) .where({ key: this.resourceForeignKeyName(request), value: resource.getAttribute(this.ownerKeyName(request)), operator: Contracts_1.Operator.eq, }) .all(); return Promise.all(detaches.map((pivot) => { return this.pivotResource .resolveRepository(request) .delete(pivot.getKey()); })); } async allowedDetachments(request, model) { return this.pivotResource .resolveRepository(request) .where({ key: this.resourceForeignKeyName(request), value: model.getAttribute(this.ownerKeyName(request)), operator: Contracts_1.Operator.eq, }) .all(); } async prepareAttachments(request, resource, requestAttribute) { return this.fillPivotFromRequest(request, requestAttribute, await this.filterAllowedAttachments(request, resource, this.getAttachments(request, requestAttribute))); } getAttachments(request, requestAttribute) { return request .array(requestAttribute, this.resolveDefaultValue(request)) .map((attachment) => { return typeof attachment === 'object' ? attachment : { id: attachment }; }); } /** * Filter attachments by policy. */ async filterAllowedAttachments(request, model, attachments) { const relatables = await this.getRelatedResources(request, attachments.map(({ id }) => id)); return attachments.filter((attachment) => { return relatables.find((relatable) => relatable.getKey() === attachment.id); }); } async getRelatedResources(request, resourceIds) { const relatedResources = await this.relatedResource .resolveRepository(request) .whereKeys(resourceIds) .all(); return this.sanitizeCallback.apply(this, [request, relatedResources]); } sanitizeUsing(sanitizeCallback) { this.sanitizeCallback = sanitizeCallback; return this; } /** * Fill pivot models. */ fillPivotFromRequest(request, requestAttribute, attachments) { const pivotFields = this.pivotFields(request); // fill pivot fields return attachments.map((related, index) => { const model = this.pivotResource.resolveRepository(request).model(); model.setAttribute(this.foreignKeyName(request), related.id); pivotFields.forEach((field) => { field.fillInto(request, model, field.attribute, `${requestAttribute}.${index}.${field.attribute}`); }); return model; }); } /** * Resolve related value for given resources. */ async resolveRelatables(request, resources) { const relatables = await this.searchRelatables(request, resources); const foreignKeyName = this.resourceForeignKeyName(request); const ownerKeyName = this.resourceOwnerKeyName(request); resources.forEach((resource) => { resource.setAttribute(this.attribute, relatables.filter((relatable) => { const pivot = relatable.getAttribute('pivot'); return (pivot.getAttribute(foreignKeyName) === resource.getAttribute(ownerKeyName)); })); }); } /** * Get related models for given resources. */ async searchRelatables(request, resources) { const pivots = await this.getPivotModels(request, resources); const relatedModels = await this.getRelatedModels(request, pivots); return pivots .map((pivot) => { const resource = relatedModels.find((related) => { return (String(related.getAttribute(this.ownerKeyName(request))) === String(pivot.getAttribute(this.foreignKeyName(request)))); }); return this.relatedResource .resolveRepository(request) .fillModel({ ...resource?.getAttributes(), pivot }); }) .filter((resource) => resource.getKey()); } /** * Get pivot records for given resources. */ async getPivotModels(request, resources) { const resourceIds = resources .map((resource) => { return resource.getAttribute(this.resourceOwnerKeyName(request)); }) .filter((value) => value); const repository = this.pivotResource.resolveRepository(request).where({ key: this.resourceForeignKeyName(request), value: resourceIds, operator: Contracts_1.Operator.in, }); // apply custom query callback this.pivotQueryCallback.apply(this, [request, repository]); return repository.all(); } /** * Get pivot records for given resources. */ async getRelatedModels(request, pivots) { const repository = this.relatedResource.resolveRepository(request).where({ key: this.ownerKeyName(request), value: pivots.map((pivot) => { return pivot.getAttribute(this.foreignKeyName(request)); }), operator: Contracts_1.Operator.in, }); const query = this.relatableQueryCallback.apply(repository, [request, repository]) ?? repository; return query.all(); } /** * Format the given related resource. */ formatRelatedResource(request, resource) { const formattedResource = super.formatRelatedResource(request, resource); const pivotFields = this.pivotFields(request); if (pivotFields.length === 0 || resource.pivot === undefined) { return formattedResource; } return { ...formattedResource, ...new FieldCollection_1.default(this.pivotFields(request)) .resolve(resource.pivot) .fieldValues(request), }; } /** * Get the swagger-ui schema. */ responseSchema(request) { const fields = new FieldCollection_1.default([ ...this.schemaFields(request), ...this.pivotFields(request), ]); return { ...super.responseSchema(request), type: 'array', default: [fields.fieldValues(request)], items: { type: 'object', properties: fields.responseSchemas(request) }, }; } /** * Get the swagger-ui schema. */ payloadSchema(request) { return { ...this.baseSchema(request), description: `use the "associable/${this.attribute}" to retrieve data`, type: 'array', items: this.pivotFields(request).length > 0 ? { type: 'object', properties: { ...this.pivotSchema(request), id: { $ref: '#components/schemas/PrimaryKey' }, }, } : { $ref: '#components/schemas/PrimaryKey' }, }; } /** * Get the pivot fields swagger-ui schema. */ pivotSchema(request) { return new FieldCollection_1.default(this.pivotFields(request)).payloadSchemas(request); } /** * Get the value considered as null. */ nullValue() { return []; } /** * Determine field is filterable or not. */ isFilterable() { return false; } } exports.default = BelongsToMany;