json-api-nestjs
Version:
JsonApi Plugin for NestJs
541 lines • 22.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MicroOrmUtilService = void 0;
const tslib_1 = require("tslib");
const common_1 = require("@nestjs/common");
const core_1 = require("@mikro-orm/core");
const knex_1 = require("@mikro-orm/knex");
const nestjs_shared_1 = require("../../../utils/nestjs-shared");
const constants_1 = require("../../../constants");
function isRelationField(relationField, field) {
if (relationField.includes(field))
return;
const error = {
code: 'unrecognized_keys',
path: ['data', 'relationships'],
message: `Resource for relation '${field.toString()}' does not exist`,
keys: [field],
};
throw new common_1.BadRequestException([error]);
}
const getErrorObject = (props, message) => ({
code: 'invalid_arguments',
path: ['data', 'relationships'],
message,
});
(0, common_1.Injectable)();
class MicroOrmUtilService {
entityManager;
entityRepository;
entity;
_relationsName;
_relationMap = new Map();
_relationPropsMap = new Map();
get relationsName() {
if (!this._relationsName) {
this._relationsName = this.metadata.relations.map((r) => r.name);
}
return this._relationsName;
}
getRelationProps(entity) {
const props = this._relationPropsMap.get(entity);
if (props) {
return props;
}
const relMetaData = this.entityManager.getMetadata(entity);
const relation = relMetaData.relations.map((i) => i.name);
const newProps = relMetaData.props
.filter((r) => !relation.includes(r.name))
.map((r) => r.name);
this._relationPropsMap.set(entity, newProps);
return newProps;
}
getRelation(name) {
let relation = this._relationMap.get(name);
if (!relation) {
relation = this.metadata.relations.find((r) => r.name === name);
if (!relation) {
throw new Error(`Relation ${name} not found in ${this.metadata.name}`);
}
this._relationMap.set(name, relation);
}
return relation;
}
get currentAlias() {
return this.entityRepository.getEntityName();
}
get metadata() {
return this.entityManager.getMetadata().get(this.entity);
}
get currentPrimaryColumn() {
return this.metadata.getPrimaryProp().name;
}
get defaultOrder() {
return {
[this.currentPrimaryColumn]: constants_1.ASC,
};
}
getAliasForEntity(entity) {
return this.entityManager.getRepository(entity).getEntityName();
}
getAliasForPivotTable(entity, relName) {
if (!relName) {
relName = entity;
entity = this.entity;
}
else {
entity = entity;
}
const propsRelation = this.entityManager
.getMetadata()
.get(entity)
.relations.find((r) => r.name.toString() === relName.toString());
if (!propsRelation)
throw new Error(`${relName.toString()} relation not found`);
if (propsRelation.kind !== core_1.ReferenceKind.MANY_TO_MANY)
throw new Error('Many to many relation expected');
return this.entityManager
.getRepository(propsRelation.pivotEntity)
.getEntityName();
}
queryBuilder(...arg) {
let [entity, alias] = arg;
if (entity && !alias) {
if (typeof entity === 'string') {
alias = entity;
entity = this.entity;
}
else {
alias = this.getAliasForEntity(entity);
}
}
if (!entity) {
alias = this.currentAlias;
entity = this.entity;
}
if (!entity || !alias) {
throw new Error('entity or alias not found');
}
return this.entityManager.createQueryBuilder(entity, alias);
}
getFilterExpressionForTarget(query) {
const result = [];
const filterTarget = this.getFilterObject(query, 'target');
const { sort, include } = query;
if (!filterTarget)
return result;
for (const [fieldName, filter] of nestjs_shared_1.ObjectTyped.entries(filterTarget)) {
const tmpField = fieldName;
if (filter === undefined)
continue;
const filterObject = {
[tmpField]: {},
};
let subQueryExpression;
for (const entries of nestjs_shared_1.ObjectTyped.entries(filter)) {
const [operand, value] = entries;
if (!this.relationsName.includes(tmpField)) {
const operandForMiroOrmResult = this.extractedResultOperand(operand);
filterObject[tmpField.toString()][operandForMiroOrmResult] = value;
continue;
}
const relation = this.getRelation(tmpField);
switch (relation.kind) {
case core_1.ReferenceKind.MANY_TO_MANY:
case core_1.ReferenceKind.ONE_TO_MANY: {
if (sort && tmpField in sort) {
filterObject[tmpField.toString()]['$exists'] =
operand === nestjs_shared_1.FilterOperand.ne;
break;
}
if (include && include.includes(tmpField)) {
filterObject[tmpField.toString()]['$exists'] =
operand === nestjs_shared_1.FilterOperand.ne;
break;
}
const subQuery = this.getSubQueryForRelation(tmpField).getFormattedQuery();
const type = operand === nestjs_shared_1.FilterOperand.ne ? 'exists' : 'not exists';
const resultQuery = `${type} (${subQuery})`;
subQueryExpression = (0, core_1.raw)(resultQuery);
break;
}
default:
filterObject[tmpField.toString()]['$exists'] =
operand === nestjs_shared_1.FilterOperand.ne;
}
}
result.push(subQueryExpression ? subQueryExpression : filterObject);
}
return result;
}
getConditionalForJoin(query, key) {
const filterRelation = this.getFilterObject(query, 'relation');
if (!filterRelation)
return {};
if (!(key in filterRelation))
return {};
for (const [key, reletionConditional] of nestjs_shared_1.ObjectTyped.entries(filterRelation)) {
if (key !== key)
continue;
if (!reletionConditional)
continue;
for (const [field, conditional] of nestjs_shared_1.ObjectTyped.entries(reletionConditional)) {
if (!conditional)
continue;
return Object.entries(conditional).reduce((acum, [operand, value]) => {
acum[field.toString()] = {
...(acum[field.toString()] || {}),
[this.extractedResultOperand(operand)]: value,
};
return acum;
}, {});
}
}
return {};
}
extractedResultOperand(operand) {
return operand === 'regexp'
? '$re'
: operand === 'some'
? '$overlap'
: ('$' + operand);
}
getFilterExpressionForRelation(query) {
const result = [];
const filterRelation = this.getFilterObject(query, 'relation');
const sort = query.sort;
if (!filterRelation)
return result;
for (const [relationField, propsFilter] of nestjs_shared_1.ObjectTyped.entries(filterRelation)) {
const fieldName = relationField;
const relationProps = this.getRelation(fieldName);
if (!propsFilter)
continue;
if (!this.relationsName.includes(fieldName))
continue;
const filterObject = {
[relationField]: {},
};
let subQueryExpression;
for (const [relationFieldProps, filter] of nestjs_shared_1.ObjectTyped.entries(propsFilter)) {
const fieldProps = relationFieldProps;
if (!filter)
continue;
for (const entries of nestjs_shared_1.ObjectTyped.entries(filter)) {
const [operand, value] = entries;
switch (relationProps.kind) {
case core_1.ReferenceKind.MANY_TO_MANY:
case core_1.ReferenceKind.ONE_TO_MANY:
case core_1.ReferenceKind.MANY_TO_ONE:
{
if (sort && relationField in sort) {
filterObject[fieldName][fieldProps] = filterObject[fieldName][fieldProps] = filterObject[fieldName][fieldProps] || {};
filterObject[fieldName][fieldProps][this.extractedResultOperand(operand)] = value;
}
else {
if (!subQueryExpression) {
subQueryExpression = this.getSubQueryForRelation(fieldName);
}
const expression = relationProps.kind === core_1.ReferenceKind.MANY_TO_MANY
? {
[this.getInverseFieldForManyToMany(fieldName)]: {
[fieldProps]: {
[this.extractedResultOperand(operand)]: value,
},
},
}
: {
[fieldProps]: {
[this.extractedResultOperand(operand)]: value,
},
};
subQueryExpression.where(expression, '$and');
}
}
break;
default:
filterObject[fieldName][fieldProps] =
filterObject[fieldName][fieldProps] || {};
filterObject[fieldName][fieldProps][this.extractedResultOperand(operand)] = value;
}
}
}
if (subQueryExpression) {
const resultQuery = `exists (${subQueryExpression.getFormattedQuery()})`;
result.push((0, core_1.raw)(resultQuery));
subQueryExpression = undefined;
}
else {
result.push(filterObject);
}
}
return result;
}
getKnex() {
return this.entityManager.getKnex();
}
prePareQueryBuilder(queryBuilder, query) {
const { fields, include } = query;
const relationFields = {};
if (fields) {
const { target, ...relations } = fields;
Object.assign(relationFields, relations);
if (target) {
if (!target.includes(this.currentPrimaryColumn)) {
target.unshift(this.currentPrimaryColumn);
}
queryBuilder.select(target);
}
}
const resultInclude = new Set([
...[...(include || []), ...nestjs_shared_1.ObjectTyped.keys(relationFields)],
]);
for (const item of resultInclude) {
const relationProps = this.getRelation(item);
const relationEntity = relationProps.entity();
const relationAlias = this.getAliasForEntity(relationEntity);
const mainAlias = this.currentAlias;
const condition = this.getConditionalForJoin(query, item);
let selectJoin = this.getRelationProps(relationEntity);
if (item in relationFields) {
const tmpSet = new Set([
...relationFields[item],
this.getPrimaryNameFor(item),
]);
selectJoin = [...tmpSet];
}
queryBuilder.leftJoinAndSelect(`${mainAlias}.${item}`, `${relationAlias}__${item}`, condition, selectJoin);
}
return queryBuilder;
}
getPrimaryNameFor(rel) {
const relationEntity = this.getRelation(rel).entity();
return this.entityManager.getMetadata().get(relationEntity).getPrimaryProp()
.name;
}
getFilterObject(query, filterType) {
const { filter } = query;
if (!filter)
return null;
return filter[filterType];
}
getSubQueryForRelation(propsName) {
const relation = this.getRelation(propsName);
let pivotTableName;
let filedCheck;
let expressionColumnName = this.currentPrimaryColumn;
if (relation.kind === core_1.ReferenceKind.MANY_TO_MANY) {
pivotTableName = this.getAliasForPivotTable(propsName);
filedCheck = relation.joinColumns.at(0);
}
else if (relation.kind === core_1.ReferenceKind.ONE_TO_MANY) {
pivotTableName = this.getAliasForEntity(relation.entity());
filedCheck = relation.mappedBy;
}
else {
expressionColumnName = relation.joinColumns.at(0);
pivotTableName = this.getAliasForEntity(relation.entity());
filedCheck = relation.referencedColumnNames.at(0);
}
if (!filedCheck)
throw new Error('filedCheck not found');
if (!expressionColumnName)
throw new Error('expressionColumnName not found');
return this.entityManager
.createQueryBuilder(pivotTableName, pivotTableName)
.select((0, core_1.raw)('1'))
.from(pivotTableName)
.where({
[filedCheck]: this.entityManager
.getKnex()
.ref(`${this.currentAlias}.${expressionColumnName}`),
});
}
getInverseFieldForManyToMany(propsName) {
const relation = this.getRelation(propsName);
const pivotTableName = this.getAliasForPivotTable(propsName);
const pivotMetaData = this.entityManager.getMetadata().get(pivotTableName);
const props = pivotMetaData.props.find((prop) => prop.targetMeta &&
prop.targetMeta.properties[propsName] &&
prop.targetMeta.properties[propsName].entity() === relation.entity());
if (!props)
throw new Error(`ManyToMany relation ${propsName} not found in ${pivotTableName}`);
return props.inversedBy || props.mappedBy;
}
createEntity(params) {
return this.entityManager.create(this.entity, params, {
partial: true,
persist: false,
});
}
async *asyncIterateFindRelationships(relationships) {
for (const entries of nestjs_shared_1.ObjectTyped.entries(relationships)) {
const [props, dataItem] = entries;
isRelationField(this.relationsName, props);
const propsKey = props;
if (dataItem === undefined)
continue;
const { data } = dataItem;
if (data === undefined)
continue;
if (data === null) {
yield { [props]: null };
continue;
}
const isArray = Array.isArray(data);
if (isArray && data.length === 0) {
yield { [props]: [] };
continue;
}
const condition = isArray
? {
$in: data.map((i) => (i || {}).id),
}
: {
$eq: data['id'],
};
const relationProps = this.metadata.properties[propsKey];
const relationEntity = relationProps.entity();
const metadata = this.entityManager.getMetadata().get(relationEntity);
const primaryName = metadata.getPrimaryProp().name;
const queryBuilder = this.queryBuilder(relationEntity, this.getAliasForEntity(relationEntity))
.select([primaryName])
.where({
[primaryName]: condition,
});
let result;
let error = undefined;
if (isArray) {
const tmpResult = await queryBuilder.getResult();
if (tmpResult.length === 0 || data.length !== tmpResult.length) {
const msg = `Resource '${metadata.className}' with ids '${data
.map((i) => (i || {})['id'])
.filter((i) => !tmpResult.find((r) => r[primaryName] == i))
.join(',')}' does not exist`;
error = new common_1.BadRequestException([getErrorObject(props, msg)]);
}
result = tmpResult;
}
else {
const tmpResult = await queryBuilder.getSingleResult();
if (!tmpResult)
error = new common_1.BadRequestException([
getErrorObject(props, `Resource '${metadata.className}' with id '${data.id}' does not exist`),
]);
result = tmpResult;
}
if (error)
throw error;
yield { [props]: result };
}
}
async saveEntity(targetInstance, relationships) {
if (relationships) {
for await (const item of this.asyncIterateFindRelationships(relationships)) {
const itemProps = nestjs_shared_1.ObjectTyped.entries(item).at(0);
if (!itemProps)
continue;
const [nameProps, data] = itemProps;
if (targetInstance[nameProps] instanceof core_1.Collection) {
targetInstance[nameProps].removeAll();
targetInstance[nameProps].add(...data);
}
else {
Object.assign(targetInstance, item);
}
}
}
await this.entityManager.persistAndFlush(targetInstance);
return targetInstance;
}
async validateRelationInputData(rel, inputData) {
const property = Reflect.get(this.metadata.properties, rel);
const isArray = Array.isArray(inputData);
if ([core_1.ReferenceKind.ONE_TO_MANY, core_1.ReferenceKind.MANY_TO_MANY].includes(property.kind) &&
!isArray) {
const error = {
code: 'invalid_arguments',
path: ['data'],
message: 'Body data should be array',
};
throw new common_1.UnprocessableEntityException([error]);
}
if ([core_1.ReferenceKind.ONE_TO_ONE, core_1.ReferenceKind.MANY_TO_ONE].includes(property.kind) &&
isArray) {
const error = {
code: 'invalid_arguments',
path: ['data'],
message: 'Body data should be object',
};
throw new common_1.UnprocessableEntityException([error]);
}
if (inputData === null) {
const result = null;
return result;
}
if (isArray && inputData.length === 0) {
const result = [];
return result;
}
const prepareData = isArray ? inputData : [inputData];
const errors = [];
let i = 0;
const relationEntity = this.getRelation(rel).entity();
const typeName = (0, nestjs_shared_1.camelToKebab)(this.entityManager.getMetadata().get(relationEntity).className);
for (const prepareItem of prepareData) {
if (prepareItem.type !== typeName) {
const path = isArray ? ['data', i.toString()] : ['data'];
errors.push({
code: 'invalid_arguments',
path: path,
message: `Type should be equal to type of relName: "${rel.toString()}". Type of ${rel.toString()} is "${typeName}" but receive - "${prepareItem.type}"`,
});
}
i++;
}
if (errors.length) {
throw new common_1.UnprocessableEntityException(errors);
}
const checkResult = await this.queryBuilder(relationEntity)
.where({
[this.getPrimaryNameFor(rel)]: {
$in: prepareData.map((i) => i.id),
},
})
.getResult();
if (checkResult.length === prepareData.length) {
return (isArray ? inputData.map((i) => i.id) : inputData.id);
}
const resulDataMap = checkResult.reduce((acum, item) => {
acum[item[this.getPrimaryNameFor(rel)]] = true;
return acum;
}, {});
i = 0;
for (const item of prepareData) {
if (!resulDataMap[item.id]) {
const path = isArray ? ['data', i.toString(), 'id'] : ['data', 'id'];
errors.push({
code: 'invalid_arguments',
path: path,
message: `Not exist item "${item.id}" in relation "${rel.toString()}"`,
});
}
i++;
}
throw new common_1.NotFoundException(errors);
}
}
exports.MicroOrmUtilService = MicroOrmUtilService;
tslib_1.__decorate([
(0, common_1.Inject)(constants_1.CURRENT_ENTITY_MANAGER_TOKEN),
tslib_1.__metadata("design:type", knex_1.SqlEntityManager)
], MicroOrmUtilService.prototype, "entityManager", void 0);
tslib_1.__decorate([
(0, common_1.Inject)(constants_1.CURRENT_ENTITY_REPOSITORY),
tslib_1.__metadata("design:type", core_1.EntityRepository)
], MicroOrmUtilService.prototype, "entityRepository", void 0);
tslib_1.__decorate([
(0, common_1.Inject)(constants_1.CURRENT_ENTITY),
tslib_1.__metadata("design:type", Object)
], MicroOrmUtilService.prototype, "entity", void 0);
//# sourceMappingURL=micro-orm-util.service.js.map