import * as mongoose from 'mongoose'; import { Schema } from 'mongoose'; import { DyFM_AnyError, DyFM_BasicProperty_Type, DyFM_DataModel_Params, DyFM_DataProperties, DyFM_DataProperty_Params, DyFM_DBFilter, DyFM_DBFilterSimple, DyFM_DBSort, DyFM_Error, DyFM_Error_Settings, DyFM_ErrorLevel, DyFM_Log, DyFM_Metadata, DyFM_Object } from '@futdevpro/fsm-dynamo'; import { DyNTS_archiveSuffix } from '../../_collections/archive.util'; import { DyNTS_DBUpdate } from '../../_models/types/db-update.type'; import { DyNTS_DBQueryOptions } from '../../_models/interfaces/db-query-options.interface'; import { DyNTS_global_settings } from '../../_collections/global-settings.const'; /** * DB Service for MongoDB */ export class DyNTS_DBService { serviceName: string; dataModel/* : mongoose.Model */; // = mongoose.model(this.dataParams.dataName, this.getSchema()); /* private depDataName: string; */ private readonly depKeys: string[] = []; defaultErrorUserMsg = `We encountered an unhandled DB Error, ` + `\nplease contact the responsible development team.`; /** * @param dataName name the model * @param typeSample sample data for scheme creation (dont include DyFM_Metadata!) * @param schemaSettings additional settings for specific parameters as unique, required, minlength or maxlength * schemaSettings also MUST contain specific types that differs from the listed above (Array, Date) */ constructor( public dataParams: DyFM_DataModel_Params ){ try { this.serviceName = this.constructor?.name; if (!this.dataParams.dataName || typeof this.dataParams.dataName !== 'string') { throw new Error( `dbName not setted up for this db-service (${this.dataParams.dataName}) (NTS DB)` ); } if (!this.dataParams.constructed?.()) { throw new DyFM_Error({ error: new Error( `DataModelParams is not constructed properly! (${this.dataParams.dataName}) ` + `Use the DyFM_DataModel_Params.constructor to construct it properly! (NTS DB)` ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-C01`, additionalContent: { dataParams: this.dataParams }, __localStack: this.dataParams.stackLocation, }); } try { this.dataModel = mongoose.model(this.dataParams.dataName, this.getSchema()); } catch (error) { if (mongoose.models[this.dataParams.dataName]) { this.dataModel = mongoose.models[this.dataParams.dataName]; } else { throw error; } } this.lookForDependencyDataSettings(); } catch (error) { throw new DyFM_Error({ message: `DyNTS_DBService construction failed! "${dataParams.dataName}" - "${this.serviceName}"`, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-C00`, level: DyFM_ErrorLevel.critical, additionalContent: { dataParams: this.dataParams }, error, }); } } /** * save new data * @param data data * @returns data */ async createData(data: T, issuer: string): Promise { data.__created = new Date(); data.__lastModified = new Date(); data.__createdBy = issuer; data.__lastModifiedBy = issuer; const dataModel = new this.dataModel(data); const newData: T = await dataModel.save().then((res): T => { if (res) { return res?.toObject() as T; } else { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'createData', new Error( `createData failed, save "${this.dataParams.dataName}" result not found! (NTS DB)` ), issuer ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-CD1`, additionalContent: { data }, issuer, }); } }).catch((error): void => { if (error?.flag?.includes('DYNAMO')) { throw error; } else { throw new DyFM_Error({ ...this._getDefaultErrorSettings('createData', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-CD0`, additionalContent: { data: data, dataModel: this.dataModel, }, message: `createData failed, Create new "${this.dataParams.dataName}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, issuer, }); } }); return this.stringifyDataId(newData, 'createData'); } /** * Find data by _id and update * @param data data * @returns data */ async modifyData(data: T, issuer: string, dontUpdateModified?: boolean): Promise { if (this.dataParams.forbidModify) { await this.modifyThrowMethod(data, issuer); } if (!dontUpdateModified) { data.__lastModified = new Date(); data.__lastModifiedBy = issuer; } /** * EZ A SZAR TELJESEN SZAR, nem friss, nem a db-be mentett adatokat ad vissza, * átír random value-kat össze vissza, WTF * */ /* let newData: T = */ await this.dataModel.findByIdAndUpdate(data._id, data)/* .then((res) => { if (res) { //return res?.toObject() as T; } else { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'modifyData', new Error(`modifyData failed, modify "${this.dataParams.dataName}" result not found! (NTS DB)`), issuer, ), status: 204, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-FU1`, additionalContent: { data }, issuer, }); } }) */.catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('modifyData', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-FU0`, additionalContent: { data }, message: `modifyData "${this.dataParams.dataName}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, issuer, }); }); return this.stringifyDataId(data, 'modifyData'); } // TODO: Missing issuer /** * returns data by _id, * @param id id * @returns data */ async getDataById(id: string): Promise { if (!id) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('getDataById', new Error(`No ID provided! (NTS DB)`)), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-GI1`, message: `get "${this.dataParams.dataName}" by ID was unsuccessful (NTS DB)`, }); } const data: T = await this.dataModel .findById(id) .then((res): T => res?.toObject() as T ?? null) .catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('getDataById', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-GI0`, additionalContent: { id }, message: `get "${this.dataParams.dataName}" by ID was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, }); }); return this.stringifyDataId(data, 'getDataById'); } // TODO: Missing issuer /** * get data by dependency data id, * !!!: throws error if not found (errorCode on not found: NTS-DBS-GD2) * * @param dependencyIdsFilter id * @returns data */ async getDataByDependencyId(dependencyIdsFilter: { [key: string]: string }): Promise { if (!this.depKeys.length) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getDataByDependencyId', new Error( `dependencyDataIdKey not setted up for this db-service (${this.dataParams.dataName}) (NTS DB)` ) ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-GD0`, }); } const data: T = await this.dataModel .findOne(dependencyIdsFilter) .then((res): T => res?.toObject() as T ?? null) .catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('getDataByDependencyId', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-GD1`, additionalContent: { dependencyIdsFilter: dependencyIdsFilter }, message: `get "${this.dataParams.dataName}" by ` + `"${Object.keys(dependencyIdsFilter).join(', ')}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, }); }); return this.stringifyDataId(data, 'getDataByDependencyId'); } // TODO: Missing issuer async getDataListByIds(ids: string[], findDeleted?: boolean): Promise { const dataList: T[] = await this.dataModel .find({ _id: { $in: ids } }) .then((res): T[] => res as T[] ?? []) .catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('getDataListByIds', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-GDL0`, additionalContent: { ids }, message: `get "${this.dataParams.dataName}" by IDs was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, }); }); if (!findDeleted) { return dataList.filter( (data: T): boolean => !data._deleted ).map( (data: T): T => this.stringifyDataId(data, 'getDataListByDependencyId') ); } else { return dataList.map( (data: T): T => this.stringifyDataId(data, 'getDataListByDependencyId') ); } } // TODO: Missing issuer /** * get data by dependency data id, * !!!: throws error if not found (errorCode on not found: NTS-DBS-GLD2) * * @param dependencyIdsFilter id * @returns dataList */ async getDataListByDependencyId(dependencyIdsFilter: { [key: string]: string }, findDeleted?: boolean): Promise { if (!this.depKeys.length) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getDataListByDependencyId', new Error( `isDependencyHook not setted up for this dataModel (${this.dataParams.dataName}) (NTS DB)` ) ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-GLD0`, additionalContent: { dataModel: this.dataModel, }, }); } const dataList: T[] = await this.dataModel .find(dependencyIdsFilter) .then((res): T[] => res as T[] ?? []) .catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('getDataListByDependencyId', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-GLD1`, additionalContent: { dependencyIdsFilter: dependencyIdsFilter }, message: `get "${this.dataParams.dataName}" by ` + `"${Object.keys(dependencyIdsFilter).join(', ')}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, }); }); if (!findDeleted) { return dataList.filter( (data: T): boolean => !data._deleted ).map( (data: T): T => this.stringifyDataId(data, 'getDataListByDependencyId') ); } else { return dataList.map( (data: T): T => this.stringifyDataId(data, 'getDataListByDependencyId') ); } } // TODO: Missing issuer /** * get multiple data objects by a list of DependencyIDs, * !!!: throws error if not found (errorCode on not found: NTS-DBS-GLDS2) * * @param ids ids * @returns dataList */ async getDataListByDependencyIds(dependencyKey: string, dependencyIds: string[], findDeleted?: boolean): Promise { if (!this.depKeys.length) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getDataListByDependencyIds', new Error( `getDataListByDependencyIds not setted up for this db-service ` + `(${this.dataParams.dataName}) (NTS DB)` ) ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-GLDS0`, }); } if (!dependencyIds?.length) { return []; } if (!dependencyKey) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getDataListByDependencyIds', new Error(`dependencyKey is missing! (${this.dataParams.dataName}) (NTS DB)`), ), }); } if (!this.depKeys.includes(dependencyKey)) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getDataListByDependencyIds', new Error(`dependencyKey is not valid! (${dependencyKey}) (NTS DB)`), ), }); } const dataList: T[] = await this.dataModel .find({ [dependencyKey]: { $in: dependencyIds }, }) .then((res): T[] => res as T[] ?? []) .catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('getDataListByDependencyIds', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-GLDS1`, additionalContent: { dependencyIds }, message: `get "${this.dataParams.dataName}" by "${dependencyKey}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, }); }); if (!findDeleted) { return dataList.filter( (data: T): boolean => !data._deleted ).map( (data: T): T => this.stringifyDataId(data, 'getDataListByDependencyIds') ); } else { return dataList.map( (data: T): T => this.stringifyDataId(data, 'getDataListByDependencyIds') ); } } // TODO: Missing issuer /** * returns all data from database, * !!!: throws error if not found (errorCode on not found: NTS-DBS-GA1) * * @returns dataList */ async getAll(findDeleted?: boolean): Promise { const dataList: T[] = await this.dataModel .find() .then((res): T[] => res as T[] ?? []) .catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('getAll', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-GA0`, message: `get all "${this.dataParams.dataName}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, }); }); if (!findDeleted) { return dataList.filter( (data: T): boolean => !data._deleted ).map( (data: T): T => this.stringifyDataId(data, 'getAll') ); } else { return dataList.map( (data: T): T => this.stringifyDataId(data, 'getAll') ); } } async markDeletedById(id: string, issuer: string): Promise { if (this.dataParams.forbidDelete) { await this.deleteThrowMethod(id, issuer); } await this.dataModel.findByIdAndUpdate( id, { _deleted: new Date(), _deletedBy: issuer, }, /* issuer */ ).catch( (error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('markDeletedById', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-MD0`, additionalContent: { id }, message: `mark deleted "${this.dataParams.dataName}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, issuer, }); } ); } async markDeletedByDependencyId(dependencyIdsFilter: { [key: string]: string }, issuer: string): Promise { if (this.dataParams.forbidDelete) { await this.deleteThrowMethod(dependencyIdsFilter, issuer); } await this.dataModel.updateMany( dependencyIdsFilter, { _deleted: new Date(), _deletedBy: issuer, } ).catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('markDeletedByDependencyId', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-MDD0`, additionalContent: { dependencyIdsFilter: dependencyIdsFilter }, message: `markDeleted "${this.dataParams.dataName}" by ` + `"${Object.keys(dependencyIdsFilter).join(', ')}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, issuer, }); }); } async restoreDeletedById(id: string, issuer: string): Promise { await this.dataModel.findByIdAndUpdate( id, { _deleted: null, __lastModified: new Date(), __lastModifiedBy: issuer, }, /* issuer */ ).catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('restoreDeletedById', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-RD0`, additionalContent: { id }, message: `restoreDeleted "${this.dataParams.dataName}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, issuer, }); }); } async restoreDeletedByDependencyId( dependencyIdsFilter: { [key: string]: string }, issuer: string ): Promise { await this.dataModel.updateMany( dependencyIdsFilter, { _deleted: null, __lastModified: new Date(), __lastModifiedBy: issuer, } ).catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('restoreDeletedByDependencyId', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-RDD0`, additionalContent: { dependencyIdsFilter: dependencyIdsFilter }, message: `restoreDeleted "${this.dataParams.dataName}" by ` + `"${Object.keys(dependencyIdsFilter).join(', ')}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, issuer, }); }); } // TODO: Missing issuer async getDeletedDataList(): Promise { const dataList: T[] = await this.dataModel .getAll() .then((res): T[] => res as T[] ?? []) .catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('getDeletedData', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-GDD0`, message: `get deleted "${this.dataParams.dataName}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, }); }); return dataList.filter( (data: T): boolean => Boolean(data._deleted) ).map((data: T): T => this.stringifyDataId(data, 'getDeletedData')); } // TODO: Missing issuer /** * deleted data by id * @param id id */ async trueDeleteDataById(id: string): Promise { if (this.dataParams.forbidDelete) { await this.deleteThrowMethod(id/* , issuer */); } await this.dataModel .findByIdAndDelete(id) .catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('deleteDataById', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-DD0`, additionalContent: { id }, message: `delete "${this.dataParams.dataName}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, }); }); } readonly deleteDataById = this.trueDeleteDataById; // TODO: Missing issuer /** * deleted data by id * @param dependencyId id */ async trueDeleteDataByDependencyId(dependencyIdsFilter: { [key: string]: string }): Promise { if (this.dataParams.forbidDelete) { await this.deleteThrowMethod(dependencyIdsFilter/* , issuer */); } if (!this.depKeys.length) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'deleteDataByDependencyId', new Error( `dependencyDataIdKey not setted up for this db-service (${this.dataParams.dataName}) (NTS DB)` ) ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-DDD0`, }); } await this.dataModel .deleteMany(dependencyIdsFilter) .catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('deleteDataByDependencyId', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-DDD1`, additionalContent: { dependencyIdsFilter: dependencyIdsFilter }, message: `delete "${this.dataParams.dataName}" by ` + `"${Object.keys(dependencyIdsFilter).join(', ')}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, }); }); } readonly deleteDataByDependencyId = this.trueDeleteDataByDependencyId; // TODO: Missing issuer async trueDeleteAllData(): Promise { if (this.dataParams.forbidDelete) { await this.deleteThrowMethod(null/* , issuer */); } await this.dataModel.deleteMany().catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('deleteAllData', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-DAD0`, message: `delete all "${this.dataParams.dataName}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, }); }); } readonly deleteAllData = this.trueDeleteAllData; // TODO: Missing issuer /** * returns search result for searchBy object params * can use lists or xRange values for searchBy obj properties * * @param filterBy filter * @param narrowByDependencyIds id * @returns dataList */ async searchData( filterBy: DyFM_DBFilter, narrowByDependencyIds?: string[], narrowByDependencyKey?: string, findDeleted?: boolean ): Promise { const filter = {}; if (0 < narrowByDependencyIds.length) { if (!this.depKeys.length) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'searchData', new Error( `dependencyDataIdKey not setted up for this db-service (${this.dataParams.dataName}) (NTS DB)` ) ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-SD0`, }); } if (!narrowByDependencyKey) { if (this.depKeys.length === 1) { narrowByDependencyKey = this.depKeys[0]; } else { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'searchData', new Error(`dependencyKey is missing! (${this.dataParams.dataName}) (NTS DB)`), ), }); } } filter[narrowByDependencyKey] = { $in: narrowByDependencyIds }; } Object.values(this.dataParams.properties).forEach( (modelParam: DyFM_DataProperty_Params): void => { if ( (filterBy[modelParam.key] !== null && filterBy[modelParam.key] !== undefined) || filterBy[modelParam.key + 'Range'] ) { if (modelParam.key.includes('Range') || modelParam.type.includes('[]')) { // inverz search filter (for Range and Array functions) if (modelParam.key.includes('Range')) { const searchParamKeyWithoutRange = modelParam.key.split('Range')[0]; if ( filterBy[searchParamKeyWithoutRange] !== null && filterBy[searchParamKeyWithoutRange] !== undefined ) { filter[modelParam.key] = { from: { $lte: filterBy[searchParamKeyWithoutRange] }, to: { $gte: filterBy[searchParamKeyWithoutRange] }, }; } } else { if ( filterBy[modelParam.key] !== null && filterBy[modelParam.key] !== undefined ) { filter[modelParam.key] = { $in: filterBy[modelParam.key] }; } } } else { // basic search filter if (filterBy[modelParam.key + 'Range']) { if ( filterBy[modelParam.key + 'Range'].from || filterBy[modelParam.key + 'Range'].to ) { filter[modelParam.key] = {}; if (filterBy[modelParam.key + 'Range'].from) { filter[modelParam.key].$gte = filterBy[modelParam.key + 'Range'].from; } if (filterBy[modelParam.key + 'Range'].to) { filter[modelParam.key].$lte = filterBy[modelParam.key + 'Range'].to; } } } else if ( filterBy[modelParam.key] !== null && filterBy[modelParam.key] !== undefined ) { if (filterBy[modelParam.key].lenght > 0) { filter[modelParam.key] = { $in: filterBy[modelParam.key] }; } } } } } ); let dataList: T[] = await this.dataModel .find(filter) .then((res): T[] => res as T[] ?? []) .catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('searchData', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-SD1`, additionalContent: { filterBy, narrowByDependencyIds }, message: `search "${this.dataParams.dataName}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, }); }); if (!findDeleted && (filterBy as DyFM_DBFilterSimple)._deleted === undefined) { dataList = dataList.filter((data: T): boolean => !data._deleted); } return dataList.map((data: T): T => this.stringifyDataId(data, 'searchData')); } // ---------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------- // DIRECT Basic MONGOOSE FUNCTIONS (with error handlings) /** * Find the data first by any of its parameters, * !!!: throws error if not found (errorCode on not found: NTS-DBS-FO1) * * @param filterBy if you can, use unique parameters for find! * * @example * // by email: * { email: email } * // * @example * // or by id that is in list: * { userIds: { $in: this.userId } } * // or by userIds: * { userId: { $in: userIds } } * // * @example * // or by number or Date that is Greater Than AND Less Than: * { points: { $gt: 2, $lt: 14 } } * // further tools (syntax matches with $gt): * $eq: // Matches values that are EQual to a specified value. * $gte: // Matches values that are Greater Than OR Equal to a specified value. * $lte: // Matches values that are Less Than or Equal to a specified value. * $ne: // Matches all values that are Not Equal to a specified value. * $nin: // Matches None of the values specified IN an array. * // * @returns {T} data: T */ async findOne(filterBy: DyFM_DBFilter): Promise { const data: T = await this.dataModel .findOne(filterBy) .then((res): T => res as T ?? null) .catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('findOne', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-FO0`, additionalContent: { filterBy }, message: `findOne "${this.dataParams.dataName}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, }); }); return this.stringifyDataId(data, 'findOne'); } /** * #MONGOOSE FUNCTION * Find the data first by any of its parameters, * !!!: throws error if not found (errorCode on not found: NTS-DBS-F1) * * @param filterBy if you can, use unique parameters for find! * * @example * // by email: * { email: email } * // * @example * // or by id that is in list: * { userIds: { $in: this.userId } } * // or by userIds: * { userId: { $in: userIds } } * // * @example * // or by number or Date that is Greater Than AND Less Than: * { points: { $gt: 2, $lt: 14 } } * // further tools (syntax matches with $gt): * $eq: // Matches values that are EQual to a specified value. * $gte: // Matches values that are Greater Than OR Equal to a specified value. * $lte: // Matches values that are Less Than or Equal to a specified value. * $ne: // Matches all values that are Not Equal to a specified value. * $nin: // Matches None of the values specified IN an array. * // * @param options BFR-008: opcionalis projection/limit/sort — DB-szinten szukit (a nagy mezok be sem toltodnek) * @returns {T[]} dataList: T[] */ async find(filterBy: DyFM_DBFilter, findDeleted?: boolean, options?: DyNTS_DBQueryOptions): Promise { // BFR-OVERSEER-008 — options megadasakor a soft-delete szures QUERY-szintre kerul: limit mellett a // memoriabeli post-filter hibas darabszamot adna, projekcio mellett (kihagyott `_deleted` mezovel) // hibasan atengedne a torolt dokumentumokat. A `_deleted` soft-delete-kor `new Date()`, egyebkent // HIANYZIK → a Mongo `_deleted: null` (missing VAGY null) a post-filter `!data._deleted` ekvivalense. const deletedGuardInQuery: boolean = !!options && !findDeleted && (filterBy as DyFM_DBFilterSimple)._deleted === undefined; const effectiveFilter: DyFM_DBFilter = deletedGuardInQuery ? { ...(filterBy as DyFM_DBFilterSimple), _deleted: null } as unknown as DyFM_DBFilter : filterBy; // A szukites (select/sort/limit) DB-szinten tortenik → a nagy mezok be sem toltodnek a heap-be. let query = this.dataModel.find(effectiveFilter); if (options?.projection) { query = query.select(options.projection); } if (options?.sort) { query = query.sort(options.sort); } if (options?.limit) { query = query.limit(options.limit); } let dataList: T[] = await query .then((res): T[] => res as T[] ?? []) .catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('find', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-F0`, additionalContent: { filterBy }, message: `find "${this.dataParams.dataName}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, }); }); if (!findDeleted && !deletedGuardInQuery && (filterBy as DyFM_DBFilterSimple)._deleted === undefined) { dataList = dataList.filter((data: T): boolean => !data._deleted); } return dataList.map((data: T): T => this.stringifyDataId(data, 'find')); } /** * #MONGOOSE FUNCTION * Find the data first by any of its parameters * WARNING: This function will not gives you back the total number of data in the database! * * @param filterBy if you can, use unique parameters for find! * * @example * // by email: * { email: email } * // * @example * // or by id that is in list: * { userIds: { $in: this.userId } } * // or by userIds: * { userId: { $in: userIds } } * // * @example * // or by number or Date that is Greater Than AND Less Than: * { points: { $gt: 2, $lt: 14 } } * // further tools (syntax matches with $gt): * $eq: // Matches values that are EQual to a specified value. * $gte: // Matches values that are Greater Than OR Equal to a specified value. * $lte: // Matches values that are Less Than or Equal to a specified value. * $ne: // Matches all values that are Not Equal to a specified value. * $nin: // Matches None of the values specified IN an array. * // * @param page page * @param pageSize pageSize * @param sort * @example * // by dateTime (this uses the basic sort function): * { dateTime: -1 } * // * @returns {T[]} dataList: T[] */ async findWithPaging( filterBy: DyFM_DBFilter, page: number, pageSize: number, sort?: DyFM_DBSort ): Promise { /* if (filterBy['_deleted'] === undefined) { filterBy['_deleted'] = null; } */ const dataList: T[] = await this.dataModel .find(filterBy) .sort(sort) .skip(page * pageSize) .limit(pageSize) .then((res): T => res ?? []) .catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('findWithPaging', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-WP0`, additionalContent: { filterBy, page, pageSize, sort }, message: `findWithPaging "${this.dataParams.dataName}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, }); }); /* if ((filterBy as DyFM_DBFilterSimple)._deleted === undefined) { dataList = dataList.filter((data: T): boolean => !data._deleted); } */ return dataList.map((data: T): T => this.stringifyDataId(data, 'find')); } /** * #MONGOOSE FUNCTION * Find data by _id and update * * @param id id * @param update update * @returns data */ async findByIdAndUpdate( id: string, update: DyNTS_DBUpdate, issuer: string, dontUpdateModified?: boolean ): Promise { if (this.dataParams.forbidModify) { await this.modifyThrowMethod(update, issuer); } if (!dontUpdateModified) { update.__lastModified = new Date(); update.__lastModifiedBy = issuer; } const data: T = await this.dataModel .findByIdAndUpdate(id, update) .then((res): T => res?.toObject() as T ?? null) .catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('findByIdAndUpdate', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-FIU0`, additionalContent: { id, update }, message: `findByIdAndUpdate "${this.dataParams.dataName}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, issuer, }); }); return this.stringifyDataId(data, 'findByIdAndUpdate'); } /** * #MONGOOSE FUNCTION * Atomi compare-and-set: filter-alapú find + update EGY műveletben, a MÓDOSÍTOTT dokumentum * visszaadásával (BFR-FDPTOKENSERVICE-006). * * A `findByIdAndUpdate` (id-only, nem filterezhető) és az `updateOne` (filterezhető, de void — * nem különíthető el a winner/loser konkurens state-átmenetnél) egyike sem elég CAS-hoz: egy * state-machine `transitionStatus`-jellegű átmenetnél a filter a FELTÉTEL (pl. `{_id, status: * 'locked'}`), és a visszatérő érték dönti el, hogy EZ a hívás nyerte-e az átmenetet. * `null` = nincs match (a feltétel már nem áll — a versenytárs nyert / state-collision). * * A `__lastModified`/`forbidModify` szemantika a `findByIdAndUpdate`-tel EGYEZŐ (egységes * update-viselkedés a base-ben); a `{ new: true }` a MÓDOSÍTOTT dokumentumot adja vissza. */ async findOneAndUpdate( filterBy: DyFM_DBFilter, update: DyNTS_DBUpdate, issuer: string, dontUpdateModified?: boolean ): Promise { if (this.dataParams.forbidModify) { await this.modifyThrowMethod(update, issuer); } if (!dontUpdateModified) { update.__lastModified = new Date(); update.__lastModifiedBy = issuer; } const data: T | null = await this.dataModel .findOneAndUpdate(filterBy, update, { new: true }) .then((res): T | null => res?.toObject() as T ?? null) .catch((error): never => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('findOneAndUpdate', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-FOU0`, additionalContent: { filterBy, update }, message: `findOneAndUpdate "${this.dataParams.dataName}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, issuer, }); }); // no-match (CAS lost / feltétel nem áll): a null ÉRVÉNYES eredmény, nem hiba if (!data) { return null; } return this.stringifyDataId(data, 'findOneAndUpdate'); } /** * #MONGOOSE FUNCTION * Find the data first by any of its parameters * * @param filter This uses the basic Mongoose updateOne. * If you can, use unique parameters for find! * @example * // by email: * { email: email } * // * @example * // or by id that is in list: * { userIds: { $in: this.userId } } * // or by userIds: * { userId: { $in: userIds } } * // * @example * // or by number or Date that is Greater Than AND Less Than: * { points: { $gt: 2, $lt: 14 } } * // further tools (syntax matches with $gt): * $eq: // Matches values that are EQual to a specified value. * $gte: // Matches values that are Greater Than OR Equal to a specified value. * $lte: // Matches values that are Less Than or Equal to a specified value. * $ne: // Matches all values that are Not Equal to a specified value. * $nin: // Matches None of the values specified IN an array. * // * * @param update this uses the basic Mongoose updateOne * @example * // increase a specific value (here by 15): * { $inc: { popularity: 15 } } * // * @example * // or add element to a list: * { $push: { reactions: this.newReaction } * // or add multiple elements to a list * { $push: { schedule: {$each: [ monday, tuesday, wednesday ] } } } * // * @example * // or all at once * { * $inc: { popularity: this.newVote.amount }, * emailVerified: true, * $push: { reactions: this.newReaction } * } * // further tools (syntax matches with $inc): * $currentDate: // Sets the value of a field to current date, either as a Date or a Timestamp. * $min: // Only updates the field if the specified value is less than the existing field value. * $max: // Only updates the field if the specified value is greater than the existing field value. * $mul: // Multiplies the value of the field by the specified amount. * $rename: // Renames a field. * $unset: // Removes the specified field from a document. (set: "" to value) * // */ async updateOne( filterBy: DyFM_DBFilter, update: DyNTS_DBUpdate, issuer: string, dontUpdateModified?: boolean ): Promise { if (this.dataParams.forbidModify) { await this.modifyThrowMethod(update, issuer); } if (!dontUpdateModified) { update.__lastModified = new Date(); update.__lastModifiedBy = issuer; } await this.dataModel .updateOne(filterBy, update) .catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('updateOne', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-UO0`, additionalContent: { filterBy, update }, message: `updateOne "${this.dataParams.dataName}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, issuer, }); }); } /** * #MONGOOSE FUNCTION * update one parameter by a specific * * @param filter This uses the basic Mongoose updateMany. * @example * // by email: * { email: email } * // * @example * // or by id that is in list: * { userIds: { $in: this.userId } } * // or by userIds: * { userId: { $in: userIds } } * // * @example * // or by number or Date that is Greater Than AND Less Than: * { points: { $gt: 2, $lt: 14 } } * // further tools (syntax matches with $gt): * $eq: // Matches values that are EQual to a specified value. * $gte: // Matches values that are Greater Than OR Equal to a specified value. * $lte: // Matches values that are Less Than or Equal to a specified value. * $ne: // Matches all values that are Not Equal to a specified value. * $nin: // Matches None of the values specified IN an array. * // * * @param update this uses the basic Mongoose updateOne * @example * // increase a specific value (here by 15): * { $inc: { popularity: 15 } } * // * @example * // or add element to a list: * { $push: { reactions: this.newReaction } * // or add multiple elements to a list * { $push: { schedule: {$each: [ monday, tuesday, wednesday ] } } } * // * @example * // or all at once * { * $inc: { popularity: this.newVote.amount }, * emailVerified: true, * $push: { reactions: this.newReaction } * } * // further tools (syntax matches with $inc): * $currentDate: // Sets the value of a field to current date, either as a Date or a Timestamp. * $min: // Only updates the field if the specified value is less than the existing field value. * $max: // Only updates the field if the specified value is greater than the existing field value. * $mul: // Multiplies the value of the field by the specified amount. * $rename: // Renames a field. * $unset: // Removes the specified field from a document. (set: "" to value) * // */ async updateMany( filterBy: DyFM_DBFilter, update: DyNTS_DBUpdate, issuer: string, dontUpdateModified?: boolean ): Promise { if (this.dataParams.forbidModify) { await this.modifyThrowMethod(update, issuer); } if (!dontUpdateModified) { update.__lastModified = new Date(); update.__lastModifiedBy = issuer; } await this.dataModel .updateMany(filterBy, update) .catch((error): void => { throw new DyFM_Error({ ...this._getDefaultErrorSettings('updateMany', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-UM0`, additionalContent: { filterBy, update }, message: `updateMany "${this.dataParams.dataName}" was unsuccessful (NTS DB)` + `\n error: ${DyFM_Error.getErrorMessage(error)}`, issuer, }); }); } /** * { "mappings": { "dynamic": true, "fields": { "embedding": { "type": "knnVector", "dimensions": 768 } } } } */ /** * * @param set db.collection.aggregate([ { $vectorSearch: { index: "index_neve", queryVector: [a keresett embedding], path: "title.embedding", numCandidates: 100, limit: 5 } }, { $vectorSearch: { index: "index_neve", queryVector: [a keresett embedding], path: "description.embedding", numCandidates: 100, limit: 5 } } ]); * @returns */ async aggregate(set: any) { return this.dataModel.aggregate(set); } // ---------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------- // PRIVATE FUNCTIONS private stringifyDataId(data: T, fnName: string): T { if (data?._id && (typeof data._id !== 'string' || typeof data._id === 'object')) { data._id = `${data._id}`; if (typeof data._id !== 'string' || typeof data._id === 'object') { data = JSON.parse(JSON.stringify(data)); if (typeof data._id !== 'string' || typeof data._id === 'object') { DyFM_Log.error( `data._id stringifying failed! Please notfiy the DynamoNTS developers! (${fnName})`, new Error() ); } } } return data; } private _getDefaultErrorSettings( fnName: string, error: DyFM_AnyError, issuer?: string ): DyFM_Error_Settings { return { status: (error as DyFM_Error)?.___status ?? 500, message: (error as Error)?.message ?? (error as DyFM_Error)?._message ?? `${fnName} was UNSUCCESSFUL (NTS-DB)`, addECToUserMsg: !(error as DyFM_Error)?.__userMessage, userMessage: (error as DyFM_Error)?.__userMessage ?? this.defaultErrorUserMsg, issuer: issuer, issuerService: this.serviceName, error: error, }; } // ---------------------------------------------------------------------------------- // PRIVATE BUILD FUNCTIONS /** * builds and returns mongoose schema * @returns schema */ private getSchema(): mongoose.Schema { this.dataParams.properties = this.addMetadataToProperties(this.dataParams.properties); if (this.dataParams.dataName.includes(DyNTS_archiveSuffix)) { this.dataParams.properties = this.addArchiveMetadataToProperties(this.dataParams.properties); } const schemaSettings: any = this.buildMongooseSchemaSettingsObjByModelParams( this.dataParams.properties ); /* schema = this.addMetadataToSchema(schema); */ /* if (this.dataParams.dataName.includes(DyNTS_archiveSuffix)) { schema = this.addArchiveMetadataToSchema(schema); } */ if (schemaSettings['_id']) { delete schemaSettings['_id']; } return new Schema(schemaSettings); } /** * builds mongoose schema building settings object by dynamoDataModelParamsList * @param properties DynamoNTSDataPropertyParams * @returns mongoose schema object */ private buildMongooseSchemaSettingsObjByModelParams(properties: DyFM_DataProperties): any { const schemaSettingsObj = {}; /* if (!properties) { properties = this.dataParams.properties; } */ Object.values(properties).forEach((property: DyFM_DataProperty_Params): void => { if (!property) { DyFM_Log.warn(`property is undefined on "${this.dataParams.dataName}" (NTS DB)`); return; } if (!property.key) { const missingKeys = Object.keys(properties).filter((key: string) => !properties[key].key); throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'buildMongooseSchemaByModelParams', new Error( `property.key is undefined on "${this.dataParams.dataName}" (NTS DB) ` + `(keys: ${Object.keys(properties).join(', ')}) ` + `(missing keys: ${missingKeys.join(', ')})` ) ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-BMSBP0`, additionalContent: { property: property, dataParams: this.dataParams, missingKeys: missingKeys, }, }); } if (property.forbidden) { return; } const beType = this.getBEType(property.type as DyFM_BasicProperty_Type); /* TODO if (beType === Array && property.archiveable && !property.archiveListAfter) { property.archiveListAfter = DyNTS_global_settings.archiveListAfter; } */ if ( beType !== Object || !property?.subObjectParams || (property?.subObjectParams && Object.keys(property.subObjectParams).length == 0) ) { schemaSettingsObj[property.key] = { type: beType, }; if (property.index) { schemaSettingsObj[property.key].index = true; } if (property.unique) { schemaSettingsObj[property.key].unique = true; } if (property.required) { schemaSettingsObj[property.key].required = true; } if (property.minlength) { schemaSettingsObj[property.key].minlength = property.minlength; } if (property.maxlength) { schemaSettingsObj[property.key].maxlength = property.maxlength; } } else { schemaSettingsObj[property.key] = this.buildMongooseSchemaSettingsObjByModelParams( property.subObjectParams ); } }); return schemaSettingsObj; } private getBEType( type: DyFM_BasicProperty_Type ): string | number | boolean | object | Function | Array | Date { switch (type) { case DyFM_BasicProperty_Type.string: return String; case DyFM_BasicProperty_Type.number: return Number; case DyFM_BasicProperty_Type.boolean: return Boolean; case DyFM_BasicProperty_Type.date: return Date; case DyFM_BasicProperty_Type.object: return Object; case DyFM_BasicProperty_Type.array: return Array; case DyFM_BasicProperty_Type.function: return Function; default: DyFM_Log.H_info('wtf', DyFM_Object.cleanObject(this.dataParams)); throw new DyFM_Error({ error: new Error( `Invalid property type: "${type}" (${this.dataParams.dataName})` + (this.dataParams.constructed?.() ? '' : ('\nThe dataParams is not constructed properly! \n' + 'Use the DyFM_DataModel_Params.constructor to construct it properly!')) ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-GTP0`, additionalContent: { type: type, dataParams: this.dataParams }, __localStack: this.dataParams.stackLocation, }); } /* switch (type.toLocaleLowerCase()) { case DyFM_BasicProperty_Type.string: case 'string': return String; case DyFM_BasicProperty_Type.number: case 'number': return Number; case DyFM_BasicProperty_Type.boolean: case 'boolean': return Boolean; case DyFM_BasicProperty_Type.date: case 'date': return Date; case DyFM_BasicProperty_Type.object: case 'object': return Object; case DyFM_BasicProperty_Type.array: case 'array': return Array; default: if (type.includes('[]') || type.includes('array')) { return Array; } else if (type.includes('=>') || type.includes('function')) { return Function; } else { return Object; } } */ } /** * adds dynamo metadata settings to any mongoose schema building settings object * @param schema schema to update * @returns updated schema */ /* private addMetadataToSchema(schema: any): any { schema.__created = { type: Date }; schema.__createdBy = { type: String }; schema.__lastModified = { type: Date }; schema.__lastModifiedBy = { type: String }; schema._deleted = { type: Date }; schema._deletedBy = { type: String }; return schema; } */ /** * adds dynamo metadata settings to any mongoose schema building settings object * @param properties properties to update * @returns updated properties */ private addMetadataToProperties(properties: DyFM_DataProperties): DyFM_DataProperties { // TOD: ezek a kikommentelt "required: true"-k kellenek, csak félő, hogy gondokat okoznak a // jelenlegi rendszerekben, ezért akkor kell majd visszarakni, ha lesz tesztelői ÉS fejlesztői kapacitás rá if (!properties.__created) { properties.__created = { key: '__created', type: DyFM_BasicProperty_Type.date/* , required: true */, }; } if (!properties.__createdBy) { properties.__createdBy = { key: '__createdBy', type: DyFM_BasicProperty_Type.string/* , required: true */, }; } if (!properties.__lastModified) { properties.__lastModified = { key: '__lastModified', type: DyFM_BasicProperty_Type.date/* , required: true */, }; } if (!properties.__lastModifiedBy) { properties.__lastModifiedBy = { key: '__lastModifiedBy', type: DyFM_BasicProperty_Type.string/* , required: true */, }; } if (!properties._deleted) { properties._deleted = { key: '_deleted', type: DyFM_BasicProperty_Type.date, }; } if (!properties._deletedBy) { properties._deletedBy = { key: '_deletedBy', type: DyFM_BasicProperty_Type.string, }; } if (!properties._archived) { properties._archived = { key: '_archived', type: DyFM_BasicProperty_Type.date, }; } return properties; } /* private addArchiveMetadataToSchema(schema: any): any { schema._originalId = { type: String, required: true, index: true, unique: true }; schema._archived = { type: Date, required: true }; return schema; } */ private addArchiveMetadataToProperties( properties: DyFM_DataProperties ): DyFM_DataProperties { if (!properties._originalId) { properties._originalId = { key: '_originalId', type: DyFM_BasicProperty_Type.string, required: true, index: true, unique: true, }; } if (!properties._archived) { properties._archived = { key: '_archived', type: DyFM_BasicProperty_Type.date, required: true, }; } else if (properties._archived.required === undefined) { properties._archived.required = true; } return properties; } /** * sets depDataKey */ private lookForDependencyDataSettings(): void { const dependencyParams: DyFM_DataProperty_Params[] = Object.values(this.dataParams.properties).filter( (modelParams: DyFM_DataProperty_Params): boolean => Boolean(modelParams.dependencyDataName) ); if (dependencyParams) { this.depKeys.push(...dependencyParams.map( (dependencyParam: DyFM_DataProperty_Params): string => dependencyParam.key )); } } private async deleteThrowMethod(data: any, issuer?: string): Promise { throw new DyFM_Error({ error: new Error(`Forbidden to delete any data! (${this.dataParams.dataName})`), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-CDAD`, additionalContent: { dataParams: this.dataParams, data: data }, issuer: issuer, }); }; private async modifyThrowMethod(data: any, issuer?: string): Promise { throw new DyFM_Error({ error: new Error(`Forbidden to modify any data! (${this.dataParams.dataName})`), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DBS-CDAD`, additionalContent: { dataParams: this.dataParams, data: data }, issuer: issuer, }); } }