@opra/sqb
Version:
Opra SQB adapter package
643 lines (642 loc) • 24.1 kB
JavaScript
"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;