UNPKG

@opra/sqb

Version:

Opra SQB adapter package

643 lines (642 loc) 24.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SqbEntityService = void 0; const common_1 = require("@opra/common"); const builder_1 = require("@sqb/builder"); const connect_1 = require("@sqb/connect"); const valgen_1 = require("valgen"); const sqb_adapter_js_1 = require("./sqb-adapter.js"); const sqb_service_base_js_1 = require("./sqb-service-base.js"); /** * @class SqbEntityService * @template T - The data type class type of the resource */ class SqbEntityService extends sqb_service_base_js_1.SqbServiceBase { /** * Constructs a new instance * * @param dataType - The data type of the returning results * @param [options] - The options for the service. * @constructor */ constructor(dataType, options) { super(options); this._inputCodecs = {}; this._outputCodecs = {}; this._dataType_ = dataType; this.resourceName = options?.resourceName; this.commonFilter = options?.commonFilter; this.interceptor = options?.interceptor; } /** * Retrieves the OPRA data type * * @throws {NotAcceptableError} If the data type is not a ComplexType. */ get dataType() { if (this._dataType && this._dataTypeScope !== this.scope) this._dataType = undefined; if (!this._dataType) this._dataType = this.context.documentNode.getComplexType(this._dataType_); this._dataTypeScope = this.scope; return this._dataType; } /** * Retrieves the Class of the data type * * @throws {NotAcceptableError} If the data type is not a ComplexType. */ get dataTypeClass() { if (!this._dataTypeClass) this._dataTypeClass = this.entityMetadata.ctor; return this._dataTypeClass; } /** * Retrieves the SQB entity metadata object * * @throws {TypeError} If metadata is not available */ get entityMetadata() { if (!this._entityMetadata) { const t = this.dataType.ctor; const metadata = connect_1.EntityMetadata.get(t); if (!metadata) throw new TypeError(`Class (${t}) is not decorated with $Entity() decorator`); this._entityMetadata = metadata; } return this._entityMetadata; } for(context, overwriteProperties, overwriteContext) { if (overwriteProperties?.commonFilter && this.commonFilter) { overwriteProperties.commonFilter = [ ...(Array.isArray(this.commonFilter) ? this.commonFilter : [this.commonFilter]), ...(Array.isArray(overwriteProperties?.commonFilter) ? overwriteProperties.commonFilter : [overwriteProperties.commonFilter]), ]; } return super.for(context, overwriteProperties, overwriteContext); } /** * Retrieves the resource name. * * @returns {string} The resource name. * @throws {Error} If the collection name is not defined. */ getResourceName() { const out = typeof this.resourceName === 'function' ? this.resourceName(this) : this.resourceName || this.dataType.name; if (out) return out; throw new Error('resourceName is not defined'); } /** * Retrieves the codec for the specified operation. * * @param operation - The operation to retrieve the encoder for. Valid values are 'create' and 'update'. */ getInputCodec(operation) { const cacheKey = operation + (this._dataTypeScope ? ':' + this._dataTypeScope : ''); let validator = this._inputCodecs[cacheKey]; if (validator) return validator; const options = { projection: '*', scope: this._dataTypeScope, }; if (operation === 'update') options.partial = 'deep'; const dataType = this.dataType; validator = dataType.generateCodec('decode', options); this._inputCodecs[cacheKey] = validator; return validator; } /** * Retrieves the codec. */ getOutputCodec(operation) { const cacheKey = operation + (this._dataTypeScope ? ':' + this._dataTypeScope : ''); let validator = this._outputCodecs[cacheKey]; if (validator) return validator; const options = { projection: '*', partial: 'deep', scope: this._dataTypeScope, }; const dataType = this.dataType; validator = dataType.generateCodec('decode', options); this._outputCodecs[cacheKey] = validator; return validator; } /** * Insert a new record into database * * @param command * @returns - A promise that resolves to the created resource * @protected */ async _create(command) { const { input, options } = command; (0, valgen_1.isNotNullish)(command.input, { label: 'input' }); const inputCodec = this.getInputCodec('create'); const outputCodec = this.getOutputCodec('create'); const data = inputCodec(input); const conn = await this.getConnection(); const repo = conn.getRepository(this.dataTypeClass); const out = await repo.create(data, options); if (out) return outputCodec(out); throw new common_1.InternalServerError(`Unknown error while creating document for "${this.getResourceName()}"`); } /** * Insert a new record into database * * @param command * @returns - A promise that resolves to the created resource * @protected */ async _createOnly(command) { const { input, options } = command; (0, valgen_1.isNotNullish)(command.input, { label: 'input' }); const inputCodec = this.getInputCodec('create'); const data = inputCodec(input); const conn = await this.getConnection(); const repo = conn.getRepository(this.dataTypeClass); return await repo.createOnly(data, options); } /** * Returns the count of records based on the provided options * * @param command * @return - A promise that resolves to the count of records * @protected */ async _count(command) { const filter = command.options?.filter ? sqb_adapter_js_1.SQBAdapter.parseFilter(command.options.filter) : undefined; return this._dbCount({ ...command.options, filter }); } /** * Deletes a record from the collection. * * @param command * @return - A Promise that resolves to the number of documents deleted. * @protected */ async _delete(command) { (0, valgen_1.isNotNullish)(command.documentId, { label: 'documentId' }); const filter = command.options?.filter ? sqb_adapter_js_1.SQBAdapter.parseFilter(command.options.filter) : undefined; return this._dbDelete(command.documentId, { ...command.options, filter }); } /** * Deletes multiple documents from the collection that meet the specified filter criteria. * * @param command * @return - A promise that resolves to the number of documents deleted. * @protected */ async _deleteMany(command) { const filter = command.options?.filter ? sqb_adapter_js_1.SQBAdapter.parseFilter(command.options.filter) : undefined; return await this._dbDeleteMany({ ...command.options, filter }); } /** * Checks if a record with the given id exists. * * @param command * @protected */ async _exists(command) { (0, valgen_1.isNotNullish)(command.documentId, { label: 'documentId' }); const filter = command.options?.filter ? sqb_adapter_js_1.SQBAdapter.parseFilter(command.options.filter) : undefined; return await this._dbExists(command.documentId, { ...command.options, filter, }); } /** * Checks if a record with the given arguments exists. * * @param command * @return - A Promise that resolves to a boolean indicating whether the record exists or not. * @protected */ async _existsOne(command) { const filter = command.options?.filter ? sqb_adapter_js_1.SQBAdapter.parseFilter(command.options.filter) : undefined; return await this._dbExistsOne({ ...command.options, filter }); } /** * Finds a record by ID. * * @param command * @return - A promise resolving to the found document, or undefined if not found. * @protected */ async _findById(command) { (0, valgen_1.isNotNullish)(command.documentId, { label: 'documentId' }); const decode = this.getOutputCodec('find'); const filter = command.options?.filter ? sqb_adapter_js_1.SQBAdapter.parseFilter(command.options.filter) : undefined; const out = await this._dbFindById(command.documentId, { ...command.options, filter, }); return out ? decode(out) : undefined; } /** * Finds a record in the collection that matches the specified options. * * @param command * @return - A promise that resolves with the found document or undefined if no document is found. * @protected */ async _findOne(command) { const decode = this.getOutputCodec('find'); const filter = command.options?.filter ? sqb_adapter_js_1.SQBAdapter.parseFilter(command.options.filter) : undefined; const out = await this._dbFindOne({ ...command.options, filter }); return out ? decode(out) : undefined; } /** * Finds multiple records in collection. * * @param command * @return - A Promise that resolves to an array of partial outputs of type T. * @protected */ async _findMany(command) { const decode = this.getOutputCodec('find'); const filter = command.options?.filter ? sqb_adapter_js_1.SQBAdapter.parseFilter(command.options.filter) : undefined; const out = await this._dbFindMany({ ...command.options, filter }); if (out?.length) { return out.map(x => decode(x)); } return out; } /** * Updates a record with the given id in the collection. * * @param command * @returns A promise that resolves to the updated document or undefined if the document was not found. * @protected */ async _update(command) { (0, valgen_1.isNotNullish)(command.documentId, { label: 'documentId' }); (0, valgen_1.isNotNullish)(command.input, { label: 'input' }); const { documentId, input, options } = command; const inputCodec = this.getInputCodec('update'); const data = inputCodec(input); const filter = command.options?.filter ? sqb_adapter_js_1.SQBAdapter.parseFilter(command.options.filter) : undefined; const out = await this._dbUpdate(documentId, data, { ...options, filter }); const outputCodec = this.getOutputCodec('update'); if (out) return outputCodec(out); } /** * Updates a record in the collection with the specified ID and returns updated record count * * @param command * @returns - A promise that resolves to the number of documents modified. * @protected */ async _updateOnly(command) { (0, valgen_1.isNotNullish)(command.documentId, { label: 'documentId' }); (0, valgen_1.isNotNullish)(command.input, { label: 'input' }); const { documentId, input, options } = command; const inputCodec = this.getInputCodec('update'); const data = inputCodec(input); const filter = command.options?.filter ? sqb_adapter_js_1.SQBAdapter.parseFilter(command.options.filter) : undefined; return await this._dbUpdateOnly(documentId, data, { ...options, filter }); } /** * Updates multiple records in the collection based on the specified input and options. * * @param command * @return - A promise that resolves to the number of documents matched and modified. * @protected */ async _updateMany(command) { (0, valgen_1.isNotNullish)(command.input, { label: 'input' }); const inputCodec = this.getInputCodec('update'); const data = inputCodec(command.input); const filter = command.options?.filter ? sqb_adapter_js_1.SQBAdapter.parseFilter(command.options.filter) : undefined; return await this._dbUpdateMany(data, { ...command.options, filter }); } /** * Acquires a connection and performs Repository.create operation * * @param input - The document to insert * @param options - Optional settings for the command * @protected */ async _dbCreate(input, options) { const conn = await this.getConnection(); const repo = conn.getRepository(this.dataTypeClass); return await repo.create(input, options); } /** * Acquires a connection and performs Repository.count operation * * @param options - The options for counting documents. * @protected */ async _dbCount(options) { const conn = await this.getConnection(); const repo = conn.getRepository(this.dataTypeClass); if (options?.filter) options.filter = sqb_adapter_js_1.SQBAdapter.parseFilter(options.filter); return await repo.count(options); } /** * Acquires a connection and performs Repository.delete operation * * @param id - Value of the key field used to select the record * @param options - Optional settings for the command * @protected */ async _dbDelete(id, options) { const conn = await this.getConnection(); const repo = conn.getRepository(this.dataTypeClass); if (options?.filter) options.filter = sqb_adapter_js_1.SQBAdapter.parseFilter(options.filter); return (await repo.delete(id, options)) ? 1 : 0; } /** * Acquires a connection and performs Repository.deleteMany operation * * @param options - Optional settings for the command * @protected */ async _dbDeleteMany(options) { const conn = await this.getConnection(); const repo = conn.getRepository(this.dataTypeClass); if (options?.filter) options.filter = sqb_adapter_js_1.SQBAdapter.parseFilter(options.filter); return await repo.deleteMany(options); } /** * Acquires a connection and performs Repository.exists operation * * @param id - Value of the key field used to select the record * @param options - Optional settings for the command * @protected */ async _dbExists(id, options) { const conn = await this.getConnection(); const repo = conn.getRepository(this.dataTypeClass); if (options?.filter) options.filter = sqb_adapter_js_1.SQBAdapter.parseFilter(options.filter); return await repo.exists(id, options); } /** * Acquires a connection and performs Repository.existsOne operation * * @param options - Optional settings for the command * @protected */ async _dbExistsOne(options) { const conn = await this.getConnection(); const repo = conn.getRepository(this.dataTypeClass); if (options?.filter) options.filter = sqb_adapter_js_1.SQBAdapter.parseFilter(options.filter); return await repo.existsOne(options); } /** * Acquires a connection and performs Repository.findById operation * * @param id - Value of the key field used to select the record * @param options - Optional settings for the command * @protected */ async _dbFindById(id, options) { const conn = await this.getConnection(); const repo = conn.getRepository(this.dataTypeClass); if (options?.filter) options.filter = sqb_adapter_js_1.SQBAdapter.parseFilter(options.filter); return await repo.findById(id, options); } /** * Acquires a connection and performs Repository.findOne operation * * @param options - Optional settings for the command * @protected */ async _dbFindOne(options) { const conn = await this.getConnection(); const repo = conn.getRepository(this.dataTypeClass); if (options?.filter) options.filter = sqb_adapter_js_1.SQBAdapter.parseFilter(options.filter); return await repo.findOne({ ...options, offset: options?.skip }); } /** * Acquires a connection and performs Repository.findMany operation * * @param options - Optional settings for the command * @protected */ async _dbFindMany(options) { const conn = await this.getConnection(); const repo = conn.getRepository(this.dataTypeClass); if (options?.filter) options.filter = sqb_adapter_js_1.SQBAdapter.parseFilter(options.filter); return await repo.findMany({ ...options, offset: options?.skip }); } /** * Acquires a connection and performs Repository.update operation * * @param id - Value of the key field used to select the record * @param data - The update values to be applied to the document * @param options - Optional settings for the command * @protected */ async _dbUpdate(id, data, options) { const conn = await this.getConnection(); const repo = conn.getRepository(this.dataTypeClass); if (options?.filter) options.filter = sqb_adapter_js_1.SQBAdapter.parseFilter(options.filter); return await repo.update(id, data, options); } /** * Acquires a connection and performs Repository.updateOnly operation * * @param id - Value of the key field used to select the record * @param data - The update values to be applied to the document * @param options - Optional settings for the command * @protected */ async _dbUpdateOnly(id, data, options) { const conn = await this.getConnection(); const repo = conn.getRepository(this.dataTypeClass); if (options?.filter) options.filter = sqb_adapter_js_1.SQBAdapter.parseFilter(options.filter); return (await repo.updateOnly(id, data, options)) ? 1 : 0; } /** * Acquires a connection and performs Repository.updateMany operation * * @param data - The update values to be applied to the document * @param options - Optional settings for the command * @protected */ async _dbUpdateMany(data, options) { const conn = await this.getConnection(); const repo = conn.getRepository(this.dataTypeClass); if (options?.filter) options.filter = sqb_adapter_js_1.SQBAdapter.parseFilter(options.filter); return await repo.updateMany(data, options); } /** * Retrieves the common filter used for querying documents. * This method is mostly used for security issues like securing multi-tenant applications. * * @protected * @returns {FilterInput | Promise<FilterInput> | undefined} The common filter or a Promise * that resolves to the common filter, or undefined if not available. */ _getCommonFilter(command) { const commonFilter = Array.isArray(this.commonFilter) ? this.commonFilter : [this.commonFilter]; const mapped = commonFilter.map(f => typeof f === 'function' ? f(command, this) : f); return mapped.length > 1 ? builder_1.op.and(...mapped) : mapped[0]; } async _executeCommand(command, commandFn) { let proto; const next = async () => { proto = proto ? Object.getPrototypeOf(proto) : this; while (proto) { if (proto.interceptor && Object.prototype.hasOwnProperty.call(proto, 'interceptor')) { return await proto.interceptor.call(this, next, command, this); } proto = Object.getPrototypeOf(proto); if (!(proto instanceof SqbEntityService)) break; } /** Call before[X] hooks */ if (command.crud === 'create') await this._beforeCreate(command); else if (command.crud === 'update' && command.byId) { await this._beforeUpdate(command); } else if (command.crud === 'update' && !command.byId) { await this._beforeUpdateMany(command); } else if (command.crud === 'delete' && command.byId) { await this._beforeDelete(command); } else if (command.crud === 'delete' && !command.byId) { await this._beforeDeleteMany(command); } /** Call command function */ return commandFn(); }; try { const result = await next(); /** Call after[X] hooks */ if (command.crud === 'create') await this._afterCreate(command, result); else if (command.crud === 'update' && command.byId) { await this._afterUpdate(command, result); } else if (command.crud === 'update' && !command.byId) { await this._afterUpdateMany(command, result); } else if (command.crud === 'delete' && command.byId) { await this._afterDelete(command, result); } else if (command.crud === 'delete' && !command.byId) { await this._afterDeleteMany(command, result); } return result; } catch (e) { Error.captureStackTrace(e, this._executeCommand); await this.onError?.(e, this); throw e; } } async _beforeCreate( // eslint-disable-next-line @typescript-eslint/no-unused-vars command) { // Do nothing } async _beforeUpdate( // eslint-disable-next-line @typescript-eslint/no-unused-vars command) { // Do nothing } async _beforeUpdateMany( // eslint-disable-next-line @typescript-eslint/no-unused-vars command) { // Do nothing } async _beforeDelete( // eslint-disable-next-line @typescript-eslint/no-unused-vars command) { // Do nothing } async _beforeDeleteMany( // eslint-disable-next-line @typescript-eslint/no-unused-vars command) { // Do nothing } async _afterCreate( // eslint-disable-next-line @typescript-eslint/no-unused-vars command, // eslint-disable-next-line @typescript-eslint/no-unused-vars result) { // Do nothing } async _afterUpdate( // eslint-disable-next-line @typescript-eslint/no-unused-vars command, // eslint-disable-next-line @typescript-eslint/no-unused-vars result) { // Do nothing } async _afterUpdateMany( // eslint-disable-next-line @typescript-eslint/no-unused-vars command, // eslint-disable-next-line @typescript-eslint/no-unused-vars affected) { // Do nothing } async _afterDelete( // eslint-disable-next-line @typescript-eslint/no-unused-vars command, // eslint-disable-next-line @typescript-eslint/no-unused-vars affected) { // Do nothing } async _afterDeleteMany( // eslint-disable-next-line @typescript-eslint/no-unused-vars command, // eslint-disable-next-line @typescript-eslint/no-unused-vars affected) { // Do nothing } } exports.SqbEntityService = SqbEntityService;