UNPKG

@e22m4u/js-repository

Version:

Реализация репозитория для работы с базами данных в Node.js

373 lines (367 loc) 11.9 kB
import {Service} from '@e22m4u/js-service'; import {RelationType} from '../definition/index.js'; import {HasOneResolver} from '../relations/index.js'; import {HasManyResolver} from '../relations/index.js'; import {WhereClauseTool} from './where-clause-tool.js'; import {OrderClauseTool} from './order-clause-tool.js'; import {SliceClauseTool} from './slice-clause-tool.js'; import {InvalidArgumentError} from '../errors/index.js'; import {BelongsToResolver} from '../relations/index.js'; import {FieldsClauseTool} from './fields-clause-tool.js'; import {ModelDefinitionUtils} from '../definition/index.js'; import {ReferencesManyResolver} from '../relations/index.js'; /** * Include clause tool. * * @typedef {string|string[]|object|object[]} IncludeClause */ export class IncludeClauseTool extends Service { /** * Include to. * * @param {object[]} entities * @param {string} modelName * @param {IncludeClause|undefined} clause * @returns {Promise<void>} */ async includeTo(entities, modelName, clause) { clause = IncludeClauseTool.normalizeIncludeClause(clause); const promises = []; clause.forEach(inclusion => { const relDef = this.getService( ModelDefinitionUtils, ).getRelationDefinitionByName(modelName, inclusion.relation); switch (relDef.type) { // BELONGS_TO case RelationType.BELONGS_TO: if (relDef.polymorphic) { promises.push( this.getService(BelongsToResolver).includePolymorphicTo( entities, modelName, inclusion.relation, relDef.foreignKey, relDef.discriminator, inclusion.scope, ), ); } else { promises.push( this.getService(BelongsToResolver).includeTo( entities, modelName, relDef.model, inclusion.relation, relDef.foreignKey, inclusion.scope, ), ); } break; // HAS_ONE case RelationType.HAS_ONE: if (relDef.polymorphic && typeof relDef.polymorphic === 'string') { promises.push( this.getService(HasOneResolver).includePolymorphicByRelationName( entities, modelName, relDef.model, inclusion.relation, relDef.polymorphic, inclusion.scope, ), ); } else if (relDef.polymorphic) { promises.push( this.getService(HasOneResolver).includePolymorphicTo( entities, modelName, relDef.model, inclusion.relation, relDef.foreignKey, relDef.discriminator, inclusion.scope, ), ); } else { promises.push( this.getService(HasOneResolver).includeTo( entities, modelName, relDef.model, inclusion.relation, relDef.foreignKey, inclusion.scope, ), ); } break; // HAS_MANY case RelationType.HAS_MANY: if (relDef.polymorphic && typeof relDef.polymorphic === 'string') { promises.push( this.getService(HasManyResolver).includePolymorphicByRelationName( entities, modelName, relDef.model, inclusion.relation, relDef.polymorphic, inclusion.scope, ), ); } else if (relDef.polymorphic) { promises.push( this.getService(HasManyResolver).includePolymorphicTo( entities, modelName, relDef.model, inclusion.relation, relDef.foreignKey, relDef.discriminator, inclusion.scope, ), ); } else { promises.push( this.getService(HasManyResolver).includeTo( entities, modelName, relDef.model, inclusion.relation, relDef.foreignKey, inclusion.scope, ), ); } break; case RelationType.REFERENCES_MANY: promises.push( this.getService(ReferencesManyResolver).includeTo( entities, modelName, relDef.model, inclusion.relation, relDef.foreignKey, inclusion.scope, ), ); break; default: throw new InvalidArgumentError( 'The relation type %v does not have an inclusion resolver.', relDef.type, ); } }); await Promise.all(promises); } /** * Validate include clause. * * @param {IncludeClause|undefined} clause */ static validateIncludeClause(clause) { if (clause == null) { // allows undefined and null } else if (clause && typeof clause === 'string') { // allows non-empty string } else if (Array.isArray(clause)) { // validate array const relNames = []; clause.flat(Infinity).forEach(el => { this.validateIncludeClause(el); if (typeof el === 'string') { relNames.push(el); } else if (typeof el === 'object') { Object.keys(el).forEach(key => { if (Object.prototype.hasOwnProperty.call(el, key)) relNames.push(key); }); } }); // duplicates checking const duplicateNames = relNames.filter( (name, i) => relNames.indexOf(name) !== i, ); if (duplicateNames.length) throw new InvalidArgumentError( 'The provided option "include" has duplicates of %v.', duplicateNames[0], ); } else if (typeof clause === 'object') { // validate object if ('relation' in clause) { // {relation: 'name', scope: {}} if (!clause.relation || typeof clause.relation !== 'string') throw new InvalidArgumentError( 'The provided option "relation" should be ' + 'a non-empty String, but %v was given.', clause.relation, ); if ('scope' in clause && clause) this.validateScopeClause(clause.scope); } else { // {foo: 'bar', 'baz': ['qux'], ...} Object.keys(clause).forEach(key => { if (!Object.prototype.hasOwnProperty.call(clause, key)) return; this.validateIncludeClause(key); this.validateIncludeClause(clause[key]); }); } } else { // unsupported throw new InvalidArgumentError( 'The provided option "include" should have a non-empty String, ' + 'an Object or an Array, but %v was given.', clause, ); } } /** * Validate scope clause. * * @param {object|undefined} clause */ static validateScopeClause(clause) { if (clause == null) return; if (typeof clause !== 'object' || Array.isArray(clause)) throw new InvalidArgumentError( 'The provided option "scope" should be an Object, but %v was given.', clause, ); // {where: ...} if (clause.where != null) { WhereClauseTool.validateWhereClause(clause.where); } // {order: ...} if (clause.order != null) { OrderClauseTool.validateOrderClause(clause.order); } // {skip: ...} if (clause.skip != null) { SliceClauseTool.validateSkipClause(clause.skip); } // {limit: ...} if (clause.limit != null) { SliceClauseTool.validateLimitClause(clause.limit); } // {fields: ...} if (clause.fields != null) { FieldsClauseTool.validateFieldsClause(clause.fields); } // {include: ...} if (clause.include != null) { IncludeClauseTool.validateIncludeClause(clause.include); } } /** * Normalize include clause. * * @param {IncludeClause|undefined} clause * @returns {object[]} */ static normalizeIncludeClause(clause) { let result = []; if (clause == null) { // allows undefined and null return result; } else if (clause && typeof clause === 'string') { // normalize non-empty string result.push({relation: clause}); } else if (Array.isArray(clause)) { // normalize array clause.flat(Infinity).forEach(el => { el = this.normalizeIncludeClause(el); result = [...result, ...el]; }); // duplicates checking const relNames = result.map(v => v.relation); const duplicateNames = relNames.filter( (name, i) => relNames.indexOf(name) !== i, ); if (duplicateNames.length) throw new InvalidArgumentError( 'The provided option "include" has duplicates of %v.', duplicateNames[0], ); } else if (typeof clause === 'object') { // normalize object if ('relation' in clause) { // {relation: 'name', scope: {...}} if (!clause.relation || typeof clause.relation !== 'string') throw new InvalidArgumentError( 'The provided option "relation" should be ' + 'a non-empty String, but %v was given.', clause.relation, ); const normalized = {relation: clause.relation}; const scope = this.normalizeScopeClause(clause.scope); if (scope) normalized.scope = scope; result.push(normalized); } else { // {foo: 'bar', baz: ['qux'], ...} Object.keys(clause).forEach(key => { if (!Object.prototype.hasOwnProperty.call(clause, key)) return; this.validateIncludeClause(key); const normalized = {relation: key}; const include = this.normalizeIncludeClause(clause[key]); if (include.length) normalized.scope = {include}; result.push(normalized); }); } } else { // unsupported throw new InvalidArgumentError( 'The provided option "include" should have a non-empty String, ' + 'an Object or an Array, but %v was given.', clause, ); } return result; } /** * Normalize scope clause. * * @param {object|undefined} clause * @returns {object|undefined} */ static normalizeScopeClause(clause) { if (clause == null) return; if (typeof clause !== 'object' || Array.isArray(clause)) throw new InvalidArgumentError( 'The provided option "scope" should be an Object, but %v was given.', clause, ); const result = {}; // {where: ...} if (clause.where != null) { WhereClauseTool.validateWhereClause(clause.where); result.where = clause.where; } // {order: ...} if (clause.order != null) { OrderClauseTool.validateOrderClause(clause.order); result.order = clause.order; } // {skip: ...} if (clause.skip != null) { SliceClauseTool.validateSkipClause(clause.skip); result.skip = clause.skip; } // {limit: ...} if (clause.limit != null) { SliceClauseTool.validateLimitClause(clause.limit); result.limit = clause.limit; } // {fields: ...} if (clause.fields != null) { FieldsClauseTool.validateFieldsClause(clause.fields); result.fields = clause.fields; } // {include: ...} if (clause.include != null) { result.include = this.normalizeIncludeClause(clause.include); } if (Object.keys(result).length) return result; return undefined; } }