@avonjs/avonjs
Version:
A fluent Node.js API generator.
384 lines (383 loc) • 14.4 kB
JavaScript
"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;