UNPKG

json-api-nestjs

Version:
541 lines 22.7 kB
"use strict"; 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