import { DyFM_AnyError, DyFM_Array, DyFM_BasicProperty_Type, DyFM_DataModel_Params, DyFM_DataProperties, DyFM_DataProperty_Params, DyFM_DBFilter, DyFM_DBFilterExpressions, DyFM_DSNestedPropertySort, DyFM_DSPropertySort, DyFM_DSSort, DyFM_Error, DyFM_Error_Settings, DyFM_ErrorLevel, DyFM_Log, DyFM_Metadata, DyFM_NestPropertySearch, DyFM_Object, DyFM_RangeValue, DyFM_SearchQuery, DyFM_SearchResult, DyFM_SpecialSearch, DyFM_String, DyFM_ValidationError, DyFM_ValidationErrors } from '@futdevpro/fsm-dynamo'; import { DyNTS_getArchivedDBName } from '../../_collections/archive.util'; import { DyNTS_global_settings } from '../../_collections/global-settings.const'; import { DyNTS_CompareData_Options } from '../../_models/interfaces/compare-data-options.interface'; import { DyNTS_CompareData_Result } from '../../_models/interfaces/compare-data-result.interface'; import { DyNTS_DBQueryOptions } from '../../_models/interfaces/db-query-options.interface'; import { DyNTS_DBUpdate } from '../../_models/types/db-update.type'; import { DyNTS_GlobalService } from '../core/global.service'; import { DyNTS_ArchiveDataService } from './archive-data.service'; import { DyNTS_archiveServiceRegistry } from '../../_collections/archive-service-registry.const'; import { DyNTS_DBService } from './db.service'; /** * `DyFM_Metadata` field-ek amik auto-discovery modban (fields opcio nelkul) * KI vannak hagyva a compareData()-bol. Ezek normal mukodes kozben kezelt * mezok (id-allocation, audit timestamps, audit usernames) — szemantikus * adatvaltozas-ellenorzeshez irrelevansak. * * Explicit `fields: ['_id']`-vel meg vizsgaltathato, ha valamiert kell. */ const COMPARE_DATA_METADATA_SKIP_FIELDS: ReadonlySet = new Set([ '_id', '__created', '__createdBy', '__lastModified', '__lastModifiedBy', ]); // TODO: 2 type of archiving service system: within list, or separate db elements /** * Basic Data Service that is connected to the relevant DBServices * * @example * export class UserMatchStatisticsService extends DyNTS_DataService { * constructor( * set: { * data?: UserMatchStatistics, * issuer: string, * } * ) { * super( * new UserMatchStatistics(set?.data), * userMatchStatisticsModelParams * ); * this.issuer = set.issuer; * } * } */ export class DyNTS_DataService { serviceName: string; serviceNameShortCode: string; /** error code base */ ecBase: string = `${DyNTS_global_settings.systemShortCodeName}|`; /** * FR-258 / SR-3 — over-fetch küszöb. Ennél több dokumentumot visszaadó {@link findDataList} hívás * az unbounded-growth → GC-thrash/OOM vektor (2026-06-23 incidens): a teljes (akár több-100k-s) * collection a heap-be töltődik. NEM capeljük a query-t (az törné a hívókat) — csak WARNINGot * logolunk, hogy az over-fetch LÁTHATÓ legyen és paginálható/szűkíthető. Runtime felülírható * (`DyNTS_DataService.findDataListWarnThreshold = N`). Default: 10000. */ static findDataListWarnThreshold: number = 10000; dataDBService: DyNTS_DBService; haveArchiveDataService: boolean; /* data: T; */ /** @deprecated */ dataList: T[] = []; /* issuer: string; */ readonly depSettings: { key: string; dbServiceKey: string; keyIsUnique: boolean; keyIsRequired: boolean; }[] = []; /* depKeys: string[] = []; depDBServiceKeys: string[] = []; depKeyIsUnique: boolean[] = []; depKeyIsRequired: boolean[] = []; */ private depDataDBService: DyNTS_DBService; /* dataParams: DyFM_DataModel_Params; */ readonly defaultErrorUserMsg: string = `We encountered an unhandled Data Service Error, ` + `\nplease contact the responsible development team.`; readonly defaultValidationErrorUserMsg: string = `We encountered an unhandled Validation Error, ` + `\nplease contact the responsible development team.`; constructor( /** * Initial data, this will be used by functions on default * @deprecated */ public data: T, /** * DB data prams will be used to connect to usable dbService on GlobalService */ /* dataParams: DyFM_DataModel_Params, */ public dataParams: DyFM_DataModel_Params, /** * Initial set for issuer to be able to follow the issuer's activity */ public issuer: string ) { try { this.serviceName = this.constructor?.name; this.serviceNameShortCode = DyFM_String.anyNameToShortCode(this.serviceName); this.ecBase = `${DyNTS_global_settings.systemShortCodeName}|${this.serviceNameShortCode}|`; this.dataDBService = DyNTS_GlobalService.getDBService(this.dataParams); /* this.data = data; */ /* this.dataParams = dataParams; */ this.haveArchiveDataService = this.dataParams.addArchive; this.lookForDependencyDataSettings(); /* this.issuer = issuer; */ /* DyNTS_GlobalService.addDataService(this); */ } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('constructor', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DS0-C00`, message: `The dataService construction failed for "${this.dataParams?.dataName}". ` + `at "${this.serviceName}" (${this.constructor.name})` + `\nMaybe you forgot to add the dbService to the GlobalService?` + `\nOr just trying to use a dependencyDataName that is not present in this db...`, level: DyFM_ErrorLevel.critical, }); /* DyFM_Log.error( `\nDyNTS_DataService ERROR: ` + `\nThe dataService construction failed for ` + `${this.dataParams?.dataName}. ${this.serviceName} (${this.constructor.name})` + `\nMaybe you forgot to add the dbService to the GlobalService?` + `\n\n`, new Error() ); */ } } getArchiveDataService(): DyNTS_ArchiveDataService { // Non-throwing default (BFR-AM-012): ha a dataParams `addArchive`-ot kér, a base ELŐÁLLÍTJA a // default archive-service-t — az `addArchive: true` így out-of-the-box működik (soft-delete → // archív-collection), per-service boilerplate-override nélkül. A korábbi feltétlen throw a // loadData/delete archive-útjain futásidőben crashelt (és async-kontextusban néma // `.split`-elfedésű rejected-promise-ként jelent meg). Override továbbra is felülírhatja. // a registry-n át példányosítunk (körkörös-import feloldás — l. archive-service-registry.const) if (this.dataParams?.addArchive && DyNTS_archiveServiceRegistry.ArchiveDataService) { return new DyNTS_archiveServiceRegistry.ArchiveDataService(this.data, this.dataParams, this.issuer); } // addArchive nélkül az archive-service-hívás valódi programhiba → marad az explicit throw throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getArchiveDataService', new Error('getArchiveDataService is not implemented! (dataParams.addArchive is not set)') ), errorCode: `${this.ecBase}DyNTS-DS-GAD0`, }); } /** * returns all data from database to service dataList */ async getAll(dontSetToService?: boolean): Promise { try { const dataListExists: T[] = await this.dataDBService.getAll().catch((error): T[] => { if (error?.errorCode?.includes('DBS-GA1')) { DyFM_Log.warn(`getAll "${this.dataParams.dataName}" didn't found any.`); return []; } else { throw error; } }); if (!dontSetToService) { this.dataList = dataListExists; } return dataListExists; } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('getAll', error), errorCode: `${this.ecBase}DyNTS-DS-GA0`, }); } } /** * @description * returns data from database by id * also if dontSetToService is false or not setted, * the data will be saved to the service, even if its not found * * if the data is not found, it will try to find it from the archive * unless skipArchiveLoad is set to true * * @remarks * If you need to get-save a data, if possible, * use db-service update instead. * * @param {string} id * (using id from service.data, if not provided) * @param dontSetToService * * @return {T} data: T */ async getDataById( id?: string, dontSetToService?: boolean, skipArchiveLoad?: boolean ): Promise { try { id = id ?? this.data?._id; if (!id) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getDataById', new Error( `getDataById failed, ID is missing! ` + `(maybe you wanted to use getDataByDependencyId() instead...) ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS-GI1`, }); } let dataExists: T = await this.dataDBService.getDataById(id); if (!dataExists && this.haveArchiveDataService && !skipArchiveLoad) { const archiveDataService: DyNTS_ArchiveDataService = this.getArchiveDataService(); dataExists = await archiveDataService.getDataByOriginalId(id, true); } if (!dontSetToService) { this.data = dataExists; } return dataExists; } catch (error) { if (error?.errorCode == `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DS0-GI1`) { throw error; } else { throw new DyFM_Error({ ...this._getDefaultErrorSettings('getDataById', error), errorCode: `${this.ecBase}DyNTS-DS-GI0`, }); } } } async getDataByIds(ids: string[], dontSetToService?: boolean): Promise { try { if (!ids) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getDataByIds', new Error(`getDataByIds failed, ids is missing! (${this.dataParams.dataName})`) ), errorCode: `${this.ecBase}DyNTS-DS-GIS1`, }); } if (ids.length === 0) { return []; } const dataList: T[] = await this.dataDBService.find({ _id: { $in: ids } }); if (!dontSetToService) { this.dataList = dataList; } return dataList; } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('getDataByIds', error), errorCode: `${this.ecBase}DyNTS-DS-GIS0`, }); } } async getDataListByIds(ids: string[], dontSetToService?: boolean): Promise { try { const dataList: T[] = await this.dataDBService.getDataListByIds(ids); if (!dontSetToService) { this.dataList = dataList; } return dataList; } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('getDataListByIds', error), errorCode: `${this.ecBase}DyNTS-DS-GIL0`, }); } } private getDependencyIdsFilter( dependencyIds?: string | { [key: string]: string } ): { [key: string]: string } { if (!this.depSettings.length) { DyFM_Log.warn( `❌ getDataByDependencyId failed, dependencyKey is missing from service! ` + `(${this.dataParams.dataName})` + `\n 📍 ${this.dataParams.stackLocation}` ); throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getDataByDependencyId', new Error( `getDataByDependencyId failed, dependencyKey is missing from service! ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS-GD1`, }); } if (typeof dependencyIds === 'string') { if (this.depSettings.length === 1) { dependencyIds = { [this.depSettings[0].key]: dependencyIds }; } else if (this.depSettings.length > 1) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getDataByDependencyId', new Error( `getDataByDependencyId failed, there are multiple dependencyKeys, ` + `so you need to provide a map of dependencyKeys! ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS-GD2`, }); } } else if (!dependencyIds) { dependencyIds = {}; this.depSettings.forEach((depSetting) => { if (this.data?.[depSetting.key]) { dependencyIds[depSetting.key] = this.data?.[depSetting.key]; } }); } if ( !dependencyIds || this.depSettings.every((depSetting) => !dependencyIds[depSetting.key]) ) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getDataByDependencyId', new Error( `getDataByDependencyId failed, dependencyId is missing! ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS-GD3`, }); } return dependencyIds as { [key: string]: string }; } /** * returns data from database by dependencyId to the service * (using id from service.data, if not provided) * @param dependencyIds */ async getDataByDependencyId( dependencyIds?: string | { [key: string]: string }, dontSetToService?: boolean ): Promise { try { const dependencyIdsFilter: { [key: string]: string } = this.getDependencyIdsFilter(dependencyIds); const dataExists: T = await this.dataDBService.getDataByDependencyId(dependencyIdsFilter).catch( (error): null => { if (error?.errorCode?.includes('DBS-GD2')) { DyFM_Log.warn( `getDataByDependencyId failed; "${this.dataParams.dataName}" ` + `(${JSON.stringify(dependencyIds)}) didn't found any.` ); return null; } else { throw error; } } ); if (!dontSetToService) { this.data = dataExists; } return dataExists; } catch (error) { if ([ `${this.ecBase}DyNTS-DS0-GD1`, `${this.ecBase}DyNTS-DS0-GD2` ].includes(error?.errorCode)) { throw error; } else { throw new DyFM_Error({ ...this._getDefaultErrorSettings('getDataByDependencyId', error), errorCode: `${this.ecBase}DyNTS-DS-GD0`, }); } } } async getDataListByDependencyIds( dependencyIds: string[], dontSetToService?: boolean, dependencyKey?: string, ): Promise { try { if (!this.depSettings.length) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getDataListByDependencyIds', new Error( `getDataListByDependencyIds failed, dependencyKey is missing from service! ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS-GDS1`, }); } if (!dependencyKey) { if (this.depSettings.length === 1) { dependencyKey = this.depSettings[0].key; } else { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getDataListByDependencyIds', new Error( `getDataListByDependencyIds failed, dependencyKey is missing! ` + `since there are multiple dependencyKeys, you need to provide one! ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS-GDS2`, }); } } if (!dependencyIds) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getDataListByDependencyIds', new Error( `getDataListByDependencyIds failed, dependencyIds is missing! ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS-GDS3`, }); } if (dependencyIds.length === 0) { return []; } const dataList: T[] = await this.dataDBService.getDataListByDependencyIds( dependencyKey, dependencyIds ); if (!dontSetToService) { this.dataList = dataList; } return dataList; } catch (error) { if ([ `${this.ecBase}DyNTS-DS0-GDS1`, `${this.ecBase}DyNTS-DS0-GDS2` ].includes(error?.errorCode)) { throw error; } else { throw new DyFM_Error({ ...this._getDefaultErrorSettings('getDataListByDependencyIds', error), errorCode: `${this.ecBase}DyNTS-DS0-GDS0`, }); } } } /** * returns dataList from database by dependencyId to the service * @param dependencyIds */ async getDataListByDependencyId( dependencyIds?: string | { [key: string]: string }, dontSetToService?: boolean ): Promise { try { const dependencyIdsFilter: { [key: string]: string } = this.getDependencyIdsFilter(dependencyIds); const dataListExists: T[] = await this.dataDBService.getDataListByDependencyId(dependencyIdsFilter).catch( (error): T[] => { if (error?.errorCode?.includes('DBS-GLD2')) { DyFM_Log.warn( `getDataListByDependencyId "${this.dataParams.dataName}" ` + `(${JSON.stringify(dependencyIdsFilter)}) didn't found any.` ); return []; } else { throw error; } } ); if (!dontSetToService) { this.dataList = dataListExists; } return dataListExists; } catch (error) { if ([ `${this.ecBase}DyNTS-DS0-GLD1`, `${this.ecBase}DyNTS-DS0-GLD2` ].includes(error?.errorCode)) { throw error; } else { throw new DyFM_Error({ ...this._getDefaultErrorSettings('getDataListByDependencyId', error), errorCode: `${this.ecBase}DyNTS-DS0-GLD0`, }); } } } /** * * // findOne desc: * * Find the data first by any of its parameters, * also if dontSetToService is false or not setted, * the data will be saved to the service, even if non found * * @param filter 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 findData(filterBy: DyFM_DBFilter, dontSetToService?: boolean): Promise { try { const dataExists: T = await this.dataDBService.findOne(filterBy).catch((error): null => { if (error?.errorCode?.includes('DBS-FO1')) { DyFM_Log.warn(`findData "${this.dataParams.dataName}" didn't found any.`); return null; } else { throw error; } }); if (!dontSetToService) { this.data = dataExists; } return dataExists; } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('findData', error), errorCode: `${this.ecBase}DyNTS-DS0-FD0`, }); } } getDataByQuery: typeof this.findData = this.findData; /** * * // find desc: * * Find the data first by any of its parameters, * also if dontSetToService is false or not setted, * the data will be saved to the service, even if non found * * @param filter 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 findDataList(filterBy: DyFM_DBFilter, dontSetToService?: boolean, options?: DyNTS_DBQueryOptions): Promise { try { // BFR-OVERSEER-008 — az opcionalis projection/limit/sort DB-szintre tovabbitodik, igy a nagy // mezok be sem toltodnek (event-loop-block + heap-churn megelozes nagy collection-oknel). const dataListExists: T[] = await this.dataDBService.find(filterBy, undefined, options).catch((error): T[] => { if (error?.errorCode?.includes('DBS-F1')) { DyFM_Log.warn(`findDataList "${this.dataParams.dataName}" didn't found any.`); return []; } else { throw error; } }); if (!dontSetToService) { this.dataList = dataListExists; } // FR-258 / SR-3 — over-fetch guard. Egy szűretlen find, ami egy nagy collection EGÉSZÉT a heap-be // tölti, a GC-thrash/OOM vektor (2026-06-23). NEM capeljük a query-t (az törné a hívókat), csak // WARNINGot logolunk, hogy az over-fetch LÁTHATÓ legyen → paginate/szűkebb filter a megoldás. const overFetchThreshold: number = DyNTS_DataService.findDataListWarnThreshold; if (overFetchThreshold > 0 && dataListExists.length > overFetchThreshold) { DyFM_Log.warn( `[FR-258 SR-3] findDataList "${this.dataParams.dataName}" returned ${dataListExists.length} documents ` + `(> ${overFetchThreshold}) — the whole result is held in heap. Consider a tighter filter or ` + `pagination (skip/limit) to avoid unbounded memory growth.`, ); } return dataListExists; } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('findDataList', error), errorCode: `${this.ecBase}DyNTS-DS0-FDS0`, }); } } /** * This function uses the dataDBService.updateOne function. * This uses updateBy if setted, or data._id if its setted or this.data[this.dependencyKey] * @param set * * // updateOne desc: * * Find the data first by any of its parameters, throws error if not found * @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 updateData( set: { filterBy?: DyFM_DBFilter, update: DyNTS_DBUpdate }, dontUpdateModified?: boolean ): Promise { try { if (set.filterBy) { await this.dataDBService.updateOne( set.filterBy, set.update, this.issuer, dontUpdateModified ); } else if (this.data._id) { await this.dataDBService.updateOne( { _id: this.data._id } as DyFM_DBFilter, set.update, this.issuer, dontUpdateModified ); } else if ( this.depSettings.length && this.depSettings.some((depSetting) => this.data[depSetting.key]) ) { const dependencyIdsFilter: { [key: string]: string } = {}; this.depSettings.forEach((depSetting) => { if (this.data[depSetting.key]) { dependencyIdsFilter[depSetting.key] = this.data[depSetting.key]; } }); await this.dataDBService.updateOne( dependencyIdsFilter as DyFM_DBFilter, set.update, this.issuer, dontUpdateModified ); } else { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'updateData', new Error( `no usable parameter provided for updateData; no updateBy, no id, no dependencyId ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-UD1`, additionalContent: { data: this.data, filterBy: set.filterBy, }, }); } } catch (error) { if (error?.errorCode?.includes('DS0-UD1')) { throw error; } else { throw new DyFM_Error({ ...this._getDefaultErrorSettings('updateData', error), errorCode: `${this.ecBase}DyNTS-DS0-UD0`, }); } } } protected ensureData(data?: T): T { if (!data && this.data) { data = this.data; } if (!data) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'ensureData', new Error(`no data to save! (${this.dataParams.dataName})`) ), errorCode: `${this.ecBase}DyNTS-DS0-ED0`, }); } return data; } async patchData(data?: Partial): Promise { try { if (!data._id) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'patchData', new Error(`no ID to patch data! (${this.dataParams.dataName})`) ), }); } const dataExists = await this.getDataById(data._id, true); if (!dataExists) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'patchData', new Error(`data not found! (${this.dataParams.dataName})`) ), }); } for (const key in this.depSettings) { if (data[key] && data[key] !== dataExists[key]) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'patchData', new Error(`Cannot patch data: dependency data mismatch! (${this.dataParams.dataName})`) ), }); } } DyFM_Object.cleanAssign(dataExists, data); await this.validateForSave(dataExists); return await this.dataDBService.modifyData(dataExists, this.issuer); } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('patchData', error), errorCode: `${this.ecBase}DyNTS-DS0-PD0`, }); } } /** * modifies data if the data have ID and already exists in the DB, * creates new if the ID is not present or cant find in DB, * and if dependency data setted up, will check before creation, * * @warning * but the proper way to update data, if you use update method instead, * this way, you can avoid data override errors * (when you simultaneously trying to change the same data's * different values from different flows) */ async saveData(data?: T, dontSetToService?: boolean, dontUpdateModified?: boolean): Promise { try { data = this.ensureData(data); await this.validateForSave(data); if (!data._id) { if ( this.depSettings.length && this.depSettings.some((depSetting) => !data[depSetting.key]) ) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'saveData', new Error( `saveData was unsuccessful: dependency data id missing from data ` + `(key: ${this.depSettings.map((depSetting) => depSetting.key).join(', ')}, ` + `${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-SD1`, additionalContent: { data: data, }, }); } // if ID of dependencyID is not present, data not exists, create new data data = await this.dataDBService.createData(data, this.issuer); if (!dontSetToService) { this.data = data; } return data; } let dataExists: T; // check if data already exists with the specific ID if (data._id) { dataExists = await this.getDataById(data._id, true); if (!dataExists) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'saveData', new Error( `saveData was unsuccessful: provided ID not exists ` + `(id: "${data._id}", ${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-SD2`, additionalContent: { data: data, }, }); } // if data exists do modify data = await this.dataDBService.modifyData(data, this.issuer, dontUpdateModified); if (!dontSetToService) { this.data = data; } return data; } if (this.depSettings.length) { if (this.depSettings.some((depSetting) => !data[depSetting.key])) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'saveData', new Error( `saveData was unsuccessful: dependency data id missing from data ` + `(key: ${this.depSettings.map((depSetting) => depSetting.key).join(', ')}, ` + `${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-SD3`, additionalContent: { data: data, }, }); } if (this.depSettings.some((depSetting) => depSetting.keyIsRequired)) { await DyFM_Array.asyncForEach( this.depSettings, async (depSetting) => { if (depSetting.keyIsRequired) { if (!data[depSetting.key]) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'saveData', new Error( `saveData was unsuccessful: dependency data id missing from data ` + `(key: ${depSetting.key}, ${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-SD4`, }); } const dependencyExists = await this.getDependencyDataDBService(depSetting.dbServiceKey) .getDataById(data[depSetting.key]) .catch(); if (!dependencyExists) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'saveData', new Error( `saveData was unsuccessful: dependency data not exists ` + `(key: ${depSetting.key}, id: "${data[depSetting.key]}", ` + `${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-SD4`, }); } } } ); } /* the db is handling the unique key, so no need to check it here if (this.depKeyIsUnique) { dataExists = await this.getDataByDependencyId(data[this.depKeys], true); if (dataExists) { // if data exists do modify data = await this.dataDBService.modifyData(data, this.issuer, dontUpdateModified); if (!dontSetToService) { this.data = data; } return data; } } */ } // if data not exists create new data data = await this.dataDBService.createData(data, this.issuer); if (!data._id) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'saveData', new Error( `saveData was unsuccessful: data creation failed ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-SD5`, additionalContent: { data: data, }, }); } if (!dontSetToService) { this.data = data; } return data; } catch (error) { if ([ `${this.ecBase}DyNTS-DS0-SD1`, `${this.ecBase}DyNTS-DS0-SD2`, `${this.ecBase}DyNTS-DS0-SD3`, `${this.ecBase}DyNTS-DS0-SD4`, `${this.ecBase}DyNTS-DS0-SD5`, ].includes(error?.errorCode)) { throw error; } else { throw new DyFM_Error({ ...this._getDefaultErrorSettings('saveData', error), errorCode: `${this.ecBase}DyNTS-DS0-SD0`, }); } } } /** * markes data as deleted, if can be archived, will be archived as well * if absolute is true, permanently deletes data from database by data._id */ async deleteData(id?: string, absolute?: boolean): Promise { try { if (!id && this.data._id) { id = this.data._id; } if (!id) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'deleteData', new Error(`deleteData failed, ID is missing! (${this.dataParams.dataName})`) ), errorCode: `${this.ecBase}DyNTS-DS0-DD1`, }); } this.data = await this.getDataById(id, true); if (!this.data) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'deleteData', new Error(`deleteData failed, data not found! (${this.dataParams.dataName})`) ), errorCode: `${this.ecBase}DyNTS-DS0-DD2`, }); } if (this.haveArchiveDataService) { const archive_DS: DyNTS_DataService = this.getArchiveDataService(); await this.dataDBService.trueDeleteDataById(id); this.data._deleted = new Date(); this.data._deletedBy = this.issuer; this.data._originalId = this.data._id; this.data._archived = new Date(); delete this.data._id; await archive_DS.saveData(this.data, true); } else if (absolute) { await this.dataDBService.trueDeleteDataById(id); delete this.data._id; } else { await this.dataDBService.markDeletedById(id, this.issuer); } } catch (error) { if (error?.errorCode?.includes('DS0-DD1')) { throw error; } else { throw new DyFM_Error({ ...this._getDefaultErrorSettings('deleteData', error), errorCode: `${this.ecBase}DyNTS-DS0-DD0`, }); } } } async deleteAllData(absolute?: boolean): Promise { try { this.dataList = await this.getAll(true); if (this.haveArchiveDataService) { const archive_DS: DyNTS_DataService = this.getArchiveDataService(); await this.dataDBService.trueDeleteAllData(); await DyFM_Array.asyncForEachAllAtOnce( this.dataList, async (data: T) => { data._deleted = new Date(); data._deletedBy = this.issuer; data._originalId = data._id; data._archived = new Date(); delete data._id; await archive_DS.saveData(data, true); } ); } else if (absolute) { await this.dataDBService.trueDeleteAllData(); this.dataList.forEach((data: T) => { delete data._id; }); } else { await DyFM_Array.asyncForEachAllAtOnce( this.dataList, async (data: T) => { await this.dataDBService.markDeletedById(data._id, this.issuer); } ); } } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('deleteAllData', error), errorCode: `${this.ecBase}DyNTS-DS0-DAD0`, }); } } restoreDeletedData(id?: string): Promise { try { if (!id && this.data._id) { id = this.data._id; } if (!id) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'restoreDeletedData', new Error(`restoreDeletedData failed, ID is missing! (${this.dataParams.dataName})`) ), errorCode: `${this.ecBase}DyNTS-DS0-RD1`, }); } let archive_DS; try { archive_DS = DyNTS_GlobalService.getDBServiceByKey( DyNTS_getArchivedDBName(this.dataParams.dataName), ); } catch (error) {} if (archive_DS) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'restoreDeletedData', new Error( `restoreDeletedData failed, archive service is setted up, ` + `archived restoration not implemented yet! ` + `(request implementation, and restore by yourself)` ) ), errorCode: `${this.ecBase}DyNTS-DS0-RD2`, }); } return this.dataDBService.restoreDeletedById(id, this.issuer); } catch (error) { if (error?.errorCode?.includes('DS0-RD1')) { throw error; } else { throw new DyFM_Error({ ...this._getDefaultErrorSettings('restoreDeletedData', error), errorCode: `${this.ecBase}DyNTS-DS0-RD0`, }); } } } /** * validation of data, for modify and create, by the ModelParams */ async validateForSave(data?: T): Promise { try { data = this.ensureData(data); if (!data) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('validateForSave', new Error('No data to validate! (validateForSave)')), errorCode: `${this.ecBase}DyNTS-DS0-VBP0`, }); } await this.validateDataByPropertyParams(data, this.dataParams.properties); } catch (error) { // a TELJES validacios csalad (VP/VDP/VBP/VD) valtozatlanul atmegy — a korabbi szukebb szuro a VDP*-t // ujra-becsomagolta (double-wrap: a 400 elveszett; BFR-FUTDEVPRO-001) if (error?.errorCode?.includes?.('DyNTS-DS0-V')) { throw error; } else { throw new DyFM_Error({ ...this._getDefaultErrorSettings('validateForSave', error), status: 400, errorCode: `${this.ecBase}DyNTS-DS0-VD0`, }); } } } private validateDataByPropertyParams( data: T, properties: DyFM_DataProperties, isSubObjectOf?: DyFM_DataProperty_Params ): void { try { if (!data) { if (isSubObjectOf?.required) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'validateDataByPropertyParams', new Error( `validateData failed, data is required! ` + `(${isSubObjectOf?.key ? `${isSubObjectOf.key}.` : ''}${this.dataParams.dataName})` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : '') ) ), errorCode: `${this.ecBase}DyNTS-DS0-VDP1`, }); } else { return; } } else if (isSubObjectOf?.forbidden) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'validateDataByPropertyParams', new Error( `validateData failed, data is forbidden! ` + `(${isSubObjectOf?.key ? `${isSubObjectOf.key}.` : ''}${this.dataParams.dataName})` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : '') ) ), errorCode: `${this.ecBase}DyNTS-DS0-VDP2`, additionalContent: { data: data, }, }); } const validationErrors: DyFM_Error[] = []; Object.values(properties).forEach( (propertyParams: DyFM_DataProperty_Params) => { try { this.validateProperty(data, data[propertyParams.key], propertyParams, isSubObjectOf); } catch (error) { validationErrors.push(error); } } ); if (validationErrors.length > 0) { if (validationErrors.length === 1) { throw validationErrors[0]; } else { const validationErrorsSet: DyFM_ValidationErrors = {}; validationErrors.forEach((error: DyFM_Error) => { Object.keys(error.validationErrors).forEach((key: string) => { validationErrorsSet[key] = error.validationErrors[key]; }); }); throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'validateByPropertyParams', new Error( `Validation failed! (multiple validation errors for ${this.dataParams.dataName})` + validationErrors.map((error: DyFM_Error) => `\n ${DyFM_Error.getAnyMessage(error)}`).join('') ) ), errorCode: `${this.ecBase}DyNTS-DS0-VDP3`, errors: validationErrors, validationErrors: validationErrorsSet, }); } } } catch (error) { // a teljes validacios csalad atengedese (a leaf VP-hibak eddig VDP0-ba csomagolodtak ujra) if (error?.errorCode?.includes?.('DyNTS-DS0-V')) { throw error; } else { throw new DyFM_Error({ ...this._getDefaultErrorSettings('validateByPropertyParams', error), status: 400, errorCode: `${this.ecBase}DyNTS-DS0-VDP0`, }); } } } private getPropertyKey( propertyParams: DyFM_DataProperty_Params, isSubObjectOf?: DyFM_DataProperty_Params ): string { return (isSubObjectOf && isSubObjectOf.key !== propertyParams.key) ? `${isSubObjectOf.key}.${propertyParams.key}` : propertyParams.key; } private validateProperty( data: T, propertyValue: any, propertyParams: DyFM_DataProperty_Params, isSubObjectOf?: DyFM_DataProperty_Params ): void { // basic required validations if ( propertyParams.required && ( propertyValue === null || propertyValue === undefined ) ) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'validateProperty', new Error( `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is missing! ` + `(required in "${this.dataParams.dataName}" dataParams) ` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : '') ) ), status: 400, errorCode: `${this.ecBase}DyNTS-DS0-VP1`, userMessage: this.defaultValidationErrorUserMsg, validationErrors: { [propertyParams.key]: { message: `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is missing! ` + `(required in "${this.dataParams.dataName}" dataParams) ` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : ''), code: `${this.ecBase}DyNTS-DS0-VP1`, userMessage: 'The property is required and must be a valid value!', }, }, __localStack: this.dataParams.stackLocation, additionalContent: { data: data, }, }); } else if ( propertyParams.forbidden && propertyValue !== undefined ) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'validateProperty', new Error( `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is forbidden! ` + `(${this.dataParams.dataName}) ` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : '') ) ), status: 400, errorCode: `${this.ecBase}DyNTS-DS0-VP2`, userMessage: this.defaultValidationErrorUserMsg, validationErrors: { [propertyParams.key]: { message: `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is forbidden! ` + `(${this.dataParams.dataName}) ` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : ''), code: `${this.ecBase}DyNTS-DS0-VP2`, userMessage: 'The property modification is forbidden!', }, }, __localStack: this.dataParams.stackLocation, additionalContent: { data: data, }, }); } // specific Date validation switch (propertyParams.type) { case DyFM_BasicProperty_Type.date: const date = new Date(propertyValue); if ( propertyParams.required && ( !(date instanceof Date) || date.toString() === 'Invalid Date' || isNaN(date.getTime()) || date.toString() === new Date(0).toString() ) ) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'validateProperty', new Error( `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is not a valid date! ` + `(${this.dataParams.dataName})` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : '') ) ), status: 400, errorCode: `${this.ecBase}DyNTS-DS0-VP3`, userMessage: this.defaultValidationErrorUserMsg, validationErrors: { [propertyParams.key]: { message: `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is not a valid date! ` + `(${this.dataParams.dataName})` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : ''), code: `${this.ecBase}DyNTS-DS0-VP3`, userMessage: 'The property is required and must be a valid date!', }, }, __localStack: this.dataParams.stackLocation, additionalContent: { data: data, }, }); } break; case DyFM_BasicProperty_Type.number: if (propertyParams.required && isNaN(propertyValue)) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'validateProperty', new Error( `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is not a valid number! ` + `(${this.dataParams.dataName})` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : '') ) ), status: 400, errorCode: `${this.ecBase}DyNTS-DS0-VP4`, userMessage: this.defaultValidationErrorUserMsg, validationErrors: { [propertyParams.key]: { message: `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is not a valid number! ` + `(${this.dataParams.dataName})` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : ''), code: `${this.ecBase}DyNTS-DS0-VP4`, userMessage: 'The property is required and must be a valid number!', }, }, __localStack: this.dataParams.stackLocation, additionalContent: { data: data, }, }); } break; case DyFM_BasicProperty_Type.string: if (propertyParams.required && typeof propertyValue !== 'string') { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'validateProperty', new Error( `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is not a valid string! ` + `(type: ${typeof propertyValue}, value: "${propertyValue}") ` + `(${this.dataParams.dataName})` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : '') ) ), status: 400, errorCode: `${this.ecBase}DyNTS-DS0-VP5`, userMessage: this.defaultValidationErrorUserMsg, validationErrors: { [propertyParams.key]: { message: `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is not a valid string! ` + `(type: ${typeof propertyValue}, value: "${propertyValue}") ` + `(${this.dataParams.dataName})` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : ''), code: `${this.ecBase}DyNTS-DS0-VP5`, userMessage: 'The property is required and must be a valid string!', }, }, __localStack: this.dataParams.stackLocation, additionalContent: { data: data, }, }); } break; case DyFM_BasicProperty_Type.boolean: if (propertyParams.required && typeof propertyValue !== 'boolean') { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'validateProperty', new Error( `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is not a valid boolean! ` + `(type: ${typeof propertyValue}, value: "${propertyValue}") ` + `(${this.dataParams.dataName})` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : '') ) ), status: 400, errorCode: `${this.ecBase}DyNTS-DS0-VP6`, userMessage: this.defaultValidationErrorUserMsg, validationErrors: { [propertyParams.key]: { message: `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is not a valid boolean! ` + `(type: ${typeof propertyValue}, value: "${propertyValue}") ` + `(${this.dataParams.dataName})` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : ''), code: `${this.ecBase}DyNTS-DS0-VP6`, userMessage: 'The property is required and must be a valid boolean!', }, }, __localStack: this.dataParams.stackLocation, }); } break; case DyFM_BasicProperty_Type.object: if (propertyParams.required && typeof propertyValue !== 'object') { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'validateProperty', new Error( `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is not a valid object! ` + `(type: ${typeof propertyValue}, value: "${propertyValue}") ` + `(${this.dataParams.dataName})` + (data as any)?.name ? `\n That failed data's name: ${(data as any).name}` : '' ) ), status: 400, errorCode: `${this.ecBase}DyNTS-DS0-VP7`, userMessage: this.defaultValidationErrorUserMsg, validationErrors: { [propertyParams.key]: { message: `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is not a valid object! ` + `(type: ${typeof propertyValue}, value: "${propertyValue}") ` + `(${this.dataParams.dataName})` + (data as any)?.name ? `\n That failed data's name: ${(data as any).name}` : '', code: `${this.ecBase}DyNTS-DS0-VP7`, userMessage: 'The property is required and must be an object!', }, }, __localStack: this.dataParams.stackLocation, }); } if (propertyParams.subObjectParams) { this.validateDataByPropertyParams(propertyValue, propertyParams.subObjectParams, propertyParams); } break; case DyFM_BasicProperty_Type.array: if (propertyParams.required && (!Array.isArray(propertyValue) || !propertyValue.length)) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'validateProperty', new Error( `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is not a valid array! ` + `(type: ${typeof propertyValue}, value: "${propertyValue}") ` + `(${this.dataParams.dataName})` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : '') ) ), status: 400, errorCode: `${this.ecBase}DyNTS-DS0-VP8`, userMessage: this.defaultValidationErrorUserMsg, validationErrors: { [propertyParams.key]: { message: `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is not a valid array! ` + `(type: ${typeof propertyValue}, value: "${propertyValue}") ` + `(${this.dataParams.dataName})` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : ''), code: `${this.ecBase}DyNTS-DS0-VP8`, userMessage: 'The property is required and must be a valid array!', }, }, __localStack: this.dataParams.stackLocation, }); } if (propertyParams.subObjectParams) { propertyValue.forEach((item: any): void => { this.validateDataByPropertyParams(item, propertyParams.subObjectParams, propertyParams); }); } break; case DyFM_BasicProperty_Type.function: DyFM_Log.warn( 'validateProperty: function type is not supported yet! ' + '(it will be skipped, but you still can use it in additionalValidators)' ); /* if (propertyParams.required && typeof propertyValue !== 'function') { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'validateProperty', new Error( `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is not a valid function! ` + `(${this.dataParams.dataName})` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : '') ) ), status: 400, errorCode: `${this.ecBase}VP9`, userMessage: this.defaultValidationErrorUserMsg, validationErrors: { [propertyParams.key]: { message: `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is not a valid function! ` + `(${this.dataParams.dataName})` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : ''), code: `${this.ecBase}DyNTS-DS0-VP9`, userMessage: 'The property is required and must be a valid function!', }, }, __localStack: this.dataParams.stackLocation, }); } */ break; default: throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'validateProperty', new Error( `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is not a valid property type! ` + `(type: ${propertyParams.type}, nonBasicType: ${propertyParams.nonBasicType}) ` + `(value type: ${typeof propertyValue}, value: "${propertyValue}") ` + `(${this.dataParams.dataName})` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : '') ) ), status: 400, errorCode: `${this.ecBase}DyNTS-DS0-VP10`, userMessage: this.defaultValidationErrorUserMsg, validationErrors: { [propertyParams.key]: { message: `validateProperty failed, "${this.getPropertyKey(propertyParams, isSubObjectOf)}" is not a valid property type! ` + `(type: ${propertyParams.type}, nonBasicType: ${propertyParams.nonBasicType}) ` + `(value type: ${typeof propertyValue}, value: "${propertyValue}") ` + `(${this.dataParams.dataName})` + ((data as any)?.name ? `\n The failed data's name: ${(data as any).name}` : ''), code: `${this.ecBase}DyNTS-DS0-VP10`, userMessage: 'The property is required and must be a valid property type!', }, }, __localStack: this.dataParams.stackLocation, }); } // call additional validators if (propertyParams.additionalValidators) { for (let j = 0; j < propertyParams.additionalValidators.length; j++) { propertyParams.additionalValidators[j]( propertyValue, data ); } } } /** * Search the data list based on the search query * Sorts and filters the data list based on the search query * * NOTE: For more simple and with better performance and if you don't need sorting, use findDataList instead. * * @example * ```ts * const searchResult = await this.searchData({ * filterBy: { * name: 'John Doe', * }, * sortBy: [ * { key: 'name', order: 1 }, * ], * }); * ``` */ async searchData(query?: DyFM_SearchQuery, dataList?: T[]): Promise> { try { const searchResult = await this.sortAndFilterDataList(query, dataList); if (query.page !== undefined && query.pageSize !== undefined) { const start = query.page * query.pageSize; const end = start + query.pageSize; searchResult.results = searchResult.results.slice(start, end); } return searchResult; } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('searchData', error), errorCode: `${this.ecBase}DyNTS-DS0-SD0`, }); } } async sortAndFilterDataList( query: DyFM_SearchQuery, dataList?: T[] ): Promise> { try { if (!query.filterBy) { query.filterBy = {}; } if (!query.page) { query.page = 0; } if (!query.pageSize) { DyFM_Log.warn( `searchData pageSize is not setted, setting to ${DyNTS_global_settings.defaultPageSize}.` ); query.pageSize = DyNTS_global_settings.defaultPageSize; } if (!query.sortBy?.length) { query.sortBy = [{ key: '__lastModified', order: -1 }]; } query.sortBy.reverse(); let preFilter: DyFM_DBFilter = {}; Object.keys(query.filterBy).forEach((key: string) => { if ( typeof query.filterBy[key] === 'object' && Object.keys(query.filterBy[key]).includes('$') ) { (preFilter as any)[key] = query.filterBy[key] as DyFM_DBFilterExpressions; delete query.filterBy[key]; } }); if (!dataList) { if (Object.keys(preFilter).length) { dataList = await this.findDataList(preFilter); } else { dataList = await this.getAll(); } } else { if (Object.keys(preFilter).length) { DyFM_Log.H_warn( 'prefilters not supported when dataList is set! (will be ignored the following keys: ' + Object.keys(preFilter).map((key: string) => `"${key}"`).join(', ') + ')' ); } } const filterKeys: string[] = Object.keys(query.filterBy); const filterFunctionsByKey: { [key: string]: (dataProperty) => boolean } = {}; const addFilterKeys: string[] = []; const removeFilterKeys: string[] = []; filterKeys.forEach((key: string): void => { const filterBy = query.filterBy[key]; if (filterBy.isSpecialNestSearch) { filterBy.nestedPropertySearches.forEach((nestSearch: DyFM_NestPropertySearch): void => { const nestKey = nestSearch.nestKeys.join('.'); filterFunctionsByKey[nestKey] = this.getFilterFunctionForKey( nestKey, nestSearch.search, nestSearch.valueType ); query.filterBy[nestKey] = nestSearch; addFilterKeys.push(nestKey); }); removeFilterKeys.push(key); } else { filterFunctionsByKey[key] = this.getFilterFunctionForKey(key, filterBy); } }); addFilterKeys.forEach((key: string): void => { filterKeys.push(key); }); removeFilterKeys.forEach((key: string): void => { filterKeys.splice(filterKeys.indexOf(key), 1); }); query.sortBy.forEach((sort: DyFM_DSSort): void => { dataList.sort(this.getSortFunctionForKey(sort)); }); if (filterKeys.some((key: string): boolean => key.includes('.'))) { const keyResolvers: { [key: string]: (data: T) => any } = {}; filterKeys.forEach((key: string): void => { if (key.includes('.')) { if (!query.filterBy[key].nestKeys) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'searchData', new Error( `searchData failed, nestKeys missing from nestedPropertySearch! ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-SD1`, }); } keyResolvers[key] = this.getKeyResolver(query.filterBy[key].nestKeys); } else { keyResolvers[key] = (data: T): any => data[key]; } }); if (filterKeys.some((key: string): boolean => query.filterBy[key].isSpecialNestSearch)) { dataList = dataList.filter( (data: T): boolean => filterKeys.every( (key: string): boolean => query.filterBy[key].isSpecialNestSearch ? keyResolvers[key](data).some( (dataProperty: any): boolean => filterFunctionsByKey[key](dataProperty) ) : filterFunctionsByKey[key](keyResolvers[key](data)) ) ); } else { dataList = dataList.filter( (data: T): boolean => filterKeys.every( (key: string): boolean => filterFunctionsByKey[key](keyResolvers[key](data)) ) ); } } else { dataList = dataList.filter( (data: T): boolean => filterKeys.every( (key: string): boolean => filterFunctionsByKey[key](data[key]) ) ); } return { results: dataList, totalItems: dataList.length, }; } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings('searchData', error), errorCode: `${this.ecBase}DyNTS-DS0-SD0`, }); } } protected getFilterFunctionForKey( key: string, searchValue: T | T[] | DyFM_RangeValue | DyFM_SpecialSearch, propertyType?: DyFM_BasicProperty_Type ): (dataProperty) => boolean { try { if (searchValue === undefined) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getFilterFunctionForKey', new Error( `getFilterFunctionForKey failed, searchValue is missing for key: "${key}" ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-GFF1`, }); } if (key.includes('.')) { if (!propertyType) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getFilterFunctionForKey', new Error( `getFilterFunctionForKey failed, propertyType is missing for key: "${key}" ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-GFF3`, }); } } else { if (!this.dataParams.properties[key]) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getFilterFunctionForKey', new Error( `getFilterFunctionForKey failed, key not found in dataParams: "${key}" ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-GFF2`, }); } propertyType = this.dataParams.properties[key].type; } switch (propertyType) { case DyFM_BasicProperty_Type.string: if (typeof searchValue !== 'string') { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getFilterFunctionForKey', new Error( `getFilterFunctionForKey failed, searchValue is not a string! ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-GFF4`, }); } if (Array.isArray(searchValue)) { const lowerCaseSearchValueArray: string[] = (searchValue as string[]).filter( (searchString: string): boolean => Boolean(searchString) ).map( (searchString: string): string => searchString.toLowerCase() ); return (dataProperty: string): boolean => { // Convert to string if not already a string (handles numbers, etc.) const dataPropertyAsString: string = dataProperty != null ? String(dataProperty) : ''; return lowerCaseSearchValueArray.some( (searchString: string): boolean => dataPropertyAsString?.toLowerCase().includes(searchString) ); }; } else { const lowerCaseSearchValue: string = (searchValue as string)?.toLowerCase(); if (!lowerCaseSearchValue) { return (dataProperty: string): boolean => !dataProperty; } return (dataProperty: string): boolean => { // Convert to string if not already a string (handles numbers, etc.) const dataPropertyAsString: string = dataProperty != null ? String(dataProperty) : ''; return dataPropertyAsString?.toLowerCase().includes(lowerCaseSearchValue); }; } case DyFM_BasicProperty_Type.date: if ((searchValue as DyFM_RangeValue).isRange) { if ( (searchValue as DyFM_RangeValue).from === undefined || (searchValue as DyFM_RangeValue).from === null ) { const toAsNumber = +new Date((searchValue as DyFM_RangeValue).to); return (dataProperty): boolean => +new Date(dataProperty) <= toAsNumber; } else if ( (searchValue as DyFM_RangeValue).to === undefined || (searchValue as DyFM_RangeValue).to === null ) { const fromAsNumber = +new Date((searchValue as DyFM_RangeValue).from); return (dataProperty): boolean => +new Date(dataProperty) >= fromAsNumber; } const rangeAsNumber = new DyFM_RangeValue( +new Date((searchValue as DyFM_RangeValue).from), +new Date((searchValue as DyFM_RangeValue).to) ); return (dataProperty): boolean => DyFM_RangeValue.isInRange( +new Date(dataProperty), rangeAsNumber ); } if (Array.isArray(searchValue)) { const searchValueAsNumberArray: number[] = (searchValue as Date[]).map( (date: Date): number => +new Date(date) ); return (dataProperty): boolean => searchValueAsNumberArray.includes(+new Date(dataProperty)); } const searchValueAsNumber: number = +new Date(searchValue as Date); return (dataProperty): boolean => +new Date(dataProperty) === searchValueAsNumber; case DyFM_BasicProperty_Type.number: if ((searchValue as DyFM_RangeValue).isRange) { if ( (searchValue as DyFM_RangeValue).from === undefined || (searchValue as DyFM_RangeValue).from === null ) { return (dataProperty): boolean => dataProperty <= (searchValue as DyFM_RangeValue).to; } else if ( (searchValue as DyFM_RangeValue).to === undefined || (searchValue as DyFM_RangeValue).to === null ) { return (dataProperty): boolean => dataProperty >= (searchValue as DyFM_RangeValue).from; } return (dataProperty): boolean => DyFM_RangeValue.isInRange( dataProperty, searchValue as DyFM_RangeValue ); } if (Array.isArray(searchValue)) { return (dataProperty): boolean => (searchValue as number[]).includes(dataProperty); } return (dataProperty): boolean => dataProperty === searchValue; case DyFM_BasicProperty_Type.boolean: return (dataProperty): boolean => dataProperty === searchValue; case DyFM_BasicProperty_Type.object: if (Array.isArray(searchValue)) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getFilterFunctionForKey', new Error( `getFilterFunctionForKey failed, array search not implemented for this type ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-GFF5`, }); } return (dataProperty): boolean => { const searchValueKeys: string[] = Object.keys(searchValue as object); const stringifiedSearchValue = {}; try { searchValueKeys.forEach( (key: string): void => { stringifiedSearchValue[key] = JSON.stringify(searchValue[key]); } ); } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getFilterFunctionForKey', new Error( `getFilterFunctionForKey failed, object search failed! ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-GFF6`, additionalContent: { searchValue: searchValue, }, }); } return searchValueKeys.every( (key: string): boolean => { try { return stringifiedSearchValue[key] === JSON.stringify(dataProperty[key]); } catch (error) { console.error( 'object filter returning false, bc of an error', 'searchValue:', searchValue, 'dataProperty:', dataProperty, 'key:', key, 'error:', error ); return false; } } ); } default: if ((searchValue as DyFM_RangeValue).isRange) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getFilterFunctionForKey', new Error( `getFilterFunctionForKey failed, range search not implemented for this type ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-GFF7`, }); } if (Array.isArray(searchValue)) { return (dataProperty): boolean => (searchValue as number[]).includes(dataProperty); } return (dataProperty): boolean => dataProperty === searchValue; } } catch (error) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getFilterFunctionForKey', error ), errorCode: `${this.ecBase}DyNTS-DS0-GFF0`, additionalContent: { key: key, searchValue: searchValue, propertyType: propertyType, }, }); } } /** * Get the value of a nested key. * @param nestKeys - The nested keys to resolve. * @returns The value of the nested key. */ private getKeyResolver(nestKeys: string[]): (data: T) => any { return (data: T): any => { let value: any = data; nestKeys.forEach((nestKey: string): void => { if (!value) { return; } if (nestKey === '*') { // Wildcard: ha value egy tömb, akkor minden elemet visszaadunk // A következő nestKey minden elemre alkalmazva lesz if (Array.isArray(value)) { // value marad tömb, a következő nestKey minden elemre alkalmazva lesz // Nem változtatjuk meg a value-t, hogy a következő iterációban // a tömb minden elemére alkalmazva legyen a következő nestKey } else if (value && typeof value === 'object') { // Ha objektum, akkor az értékeit adja vissza tömbként value = Object.values(value); } else { // Egyébként undefined value = undefined; } } else if (Array.isArray(value)) { // Ha value tömb, akkor minden elemre alkalmazzuk a nestKey-t if (!isNaN(+nestKey)) { value = value[+nestKey]; } else { value = value.map((element: any): any => element ? element[nestKey] : element); } } else { value = value[nestKey]; } }); return value; }; } /** * Get the sort function for a key. * @param sortSettings - The sort settings. * @returns The sort function for the key. */ protected getSortFunctionForKey(sortSettings: DyFM_DSSort): (a: T, b: T) => number { let keyResolver: (data: any) => any; let valueType: DyFM_BasicProperty_Type; if ( !this.dataParams.properties[(sortSettings as DyFM_DSPropertySort).key] && !(sortSettings as DyFM_DSNestedPropertySort).isNestedPropertySort ) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getSortFunctionForKey', new Error( `getSortFunctionForKey failed, key not found in dataParams ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-GSF1`, }); } if ((sortSettings as DyFM_DSNestedPropertySort).isNestedPropertySort) { keyResolver = this.getKeyResolver((sortSettings as DyFM_DSNestedPropertySort).nestKeys); valueType = (sortSettings as DyFM_DSNestedPropertySort).valueType; } else { keyResolver = (data: T): any => data[(sortSettings as DyFM_DSPropertySort).key]; valueType = this.dataParams.properties[(sortSettings as DyFM_DSPropertySort).key].type; } if (!valueType) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getSortFunctionForKey', new Error( `getSortFunctionForKey failed, valueType is missing! ` + `(${this.dataParams.dataName})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-GSF1`, }); } const sortValue: 1 | -1 = ( sortSettings.order === 1 || sortSettings.order === 'asc' || sortSettings.order === 'ascending' ) ? 1 : -1; switch (valueType) { case DyFM_BasicProperty_Type.string: return (a: T, b: T): number => { a = keyResolver(a); b = keyResolver(b); return (a as string).localeCompare(b as string) * sortValue; } case DyFM_BasicProperty_Type.date: return (a: T, b: T): number => { const aNum = +new Date(keyResolver(a)); const bNum = +new Date(keyResolver(b)); if (aNum < bNum) { return -sortValue; } else if (aNum > bNum) { return sortValue; } else { return 0; } }; case DyFM_BasicProperty_Type.number: return (a: T, b: T): number => { a = keyResolver(a); b = keyResolver(b); if (a < b) { return -sortValue; } else if (a > b) { return sortValue; } else { return 0; } }; case DyFM_BasicProperty_Type.boolean: return (a: T, b: T): number => { a = keyResolver(a); b = keyResolver(b); if (a === b) { return 0; } if (a) { return sortValue; } return -sortValue; }; default: throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getSortFunctionForKey', new Error( `getSortFunctionForKey failed, sorting not implemented for this type ` + `(${this.dataParams.dataName}, ${valueType})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-GSF2`, }); } } /* private getSortFunctionForNestedKeys( sortSettings: DyFM_DSNestedPropertySort ): (a: T, b: T) => number { const nestedPropertyResolver: (data) => any = this.getKeyResolver(sortSettings.nestKeys); const sortValue: 1 | -1 = ( sortSettings.order === 1 || sortSettings.order === 'asc' || sortSettings.order === 'ascending' ) ? 1 : -1; switch (sortSettings.valueType) { case DyFM_BasicProperty_Type.string: return (a: T, b: T): number => { a = nestedPropertyResolver(a); b = nestedPropertyResolver(b); return (a as string).localeCompare(b as string) * sortValue }; case DyFM_BasicProperty_Type.date: return (a: T, b: T): number => { a = nestedPropertyResolver(a); b = nestedPropertyResolver(b); if (+new Date(a) < +new Date(b)) { return -sortValue; } else if (+new Date(a) > +new Date(b)) { return sortValue; } else { return 0; } }; case DyFM_BasicProperty_Type.number: return (a: T, b: T): number => { a = nestedPropertyResolver(a); b = nestedPropertyResolver(b); if (a < b) { return -sortValue; } else if (a > b) { return sortValue; } else { return 0; } }; case DyFM_BasicProperty_Type.boolean: return (a: T, b: T): number => { a = nestedPropertyResolver(a); b = nestedPropertyResolver(b); if (a === b) { return 0; } if (a) { return sortValue; } return -sortValue; }; default: throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getSortFunctionForKey', new Error( `getSortFunctionForKey failed, sorting not implemented for this type ` + `(${this.dataParams.dataName}, ${this.dataParams.properties[key].type})` ) ), errorCode: `${this.ecBase}DyNTS-DS0-GSF2`, }); } } */ /** * setting up dependency dataHook by DynamoNTSDataModelParams */ private lookForDependencyDataSettings(): void { const dependencyParams: DyFM_DataProperty_Params[] = Object.values(this.dataParams.properties).filter( (modelParams: DyFM_DataProperty_Params): boolean => Boolean(modelParams.dependencyDataName) ); this.depSettings.push(...dependencyParams.map((dependencyParams) => ({ key: dependencyParams.key, dbServiceKey: dependencyParams.dependencyDataName, keyIsUnique: dependencyParams.unique, keyIsRequired: dependencyParams.required, }))); if (this.depSettings.length === 1) { this.depDataDBService = DyNTS_GlobalService.getDBServiceByKey( this.depSettings[0].dbServiceKey ); } } /** * * @returns */ getDependencyDataDBService(dBServiceKey?: string): DyNTS_DBService { if (!dBServiceKey && !this.depDataDBService) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'getDependencyDataDBService', new Error( `getDependencyDataDBService was unsuccessful, service key not setted up! ` + `(key: ${this.depSettings.map((depSetting) => depSetting.key).join(', ')}, ` + `(${this.dataParams.dataName})` ) ), status: 500, errorCode: `${this.ecBase}DyNTS-DS0-GDDB0`, }); } if (this.depDataDBService) { return this.depDataDBService; } else { this.depDataDBService = DyNTS_GlobalService.getDBServiceByKey(dBServiceKey); return this.depDataDBService; } } getProvidedData(data: T): T { return data; } getProvidedDataList(dataList: T[]): T[] { return dataList; } private _getDefaultErrorSettings( fnName: string, error: DyFM_AnyError ): 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; ${this.dataParams.dataName})`, addECToUserMsg: !(error as DyFM_Error)?.__userMessage, userMessage: (error as DyFM_Error)?.__userMessage ?? this.defaultErrorUserMsg, issuer: this.issuer, issuerService: this.serviceName + ` (${this?.constructor?.name}-DyNTS_DataService)`, systemVersion: DyNTS_global_settings.systemVersion, error: error, }; } protected getDefaultErrorSettings( fnName: string, error: DyFM_AnyError, /** @deprecated we wont support the separate user message in the future, use the message instead */ useMessageAsUserMessage: boolean = true, ): DyFM_Error_Settings { return { status: (error as DyFM_Error)?.___status ?? 500, message: (error as Error)?.message ?? (error as DyFM_Error)?._message ?? `${fnName} was UNSUCCESSFUL (${DyNTS_global_settings.systemShortCodeName})`, addECToUserMsg: !(error as DyFM_Error)?.__userMessage, userMessage: (error as DyFM_Error)?.__userMessage ?? (useMessageAsUserMessage ? (error as Error)?.message : this.defaultErrorUserMsg), issuer: this.issuer, issuerService: this.constructor?.name, systemVersion: DyNTS_global_settings.systemVersion, error: error, }; } // ════════════════════════════════════════════════════════════════════════ // GENERIC DATA COMPARE (FR-001) // ════════════════════════════════════════════════════════════════════════ /** * Generic, field-szintu osszehasonlitas ket adat-objektum kozott. * * **Auto-discovery mod** (`options.fields` nelkul): a metodus mindket * objektum kulcs-uniojan iter, KIVEVE a `DyFM_Metadata` skip-listet * (`_id`, `__created`, `__createdBy`, `__lastModified`, `__lastModifiedBy`). * * **Scope-szukito mod** (`options.fields = [...]`): KIZAROLAG a felsorolt * mezoket vizsgalja. A skip-list IGNORALODIK — explicit fields override. * * **Custom comparator** (`options.customComparators`): per-field override * a default deep-equal helyett (pl. set-equality array-ekre, * case-insensitive string compare-re). A custom function `true`-val signal-ozza * az equality-t. * * **Return:** `'equal'` ha minden vizsgalt field egyezik; `'modified'` ha * legalabb egy elter — ilyenkor `changedFields` tartalmazza a TELJES * mismatch-listat (nem early-return). * * **Throws:** * - `newData` vagy `oldData` null/undefined → `DyFM_Error(400, DyNTS-DS0-CD1)` * - `options.fields = []` ures array → `DyFM_Error(400, DyNTS-DS0-CD2)` * * **Sync method** (nincs I/O szukseglet); a host wrappel-i ha async kell. * * @example * const result = userService.compareData(newUser, oldUser); * if (result.result === 'modified') { * console.log('Changed:', result.changedFields); * } * * @example // Scope-szukites + custom comparator * userService.compareData(newUser, oldUser, { * fields: ['email', 'roles'], * customComparators: { * roles: (a, b) => new Set(a).size === new Set([...a, ...b]).size, * }, * }); */ compareData( newData: T, oldData: T, options?: DyNTS_CompareData_Options, ): DyNTS_CompareData_Result { // Input guard if (newData === null || newData === undefined || oldData === null || oldData === undefined) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('compareData', new Error('newData/oldData required')), status: 400, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DS0-CD1`, message: 'compareData: newData and oldData are required (both must be non-null/undefined)', }); } if (options?.fields !== undefined && options.fields.length === 0) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('compareData', new Error('options.fields must not be empty')), status: 400, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DS0-CD2`, message: 'compareData: options.fields must not be an empty array', }); } const fieldsToCheck: (keyof T)[] = this._resolveCompareFields(newData, oldData, options); const comparators: Partial boolean>> = options?.customComparators ?? {}; const changedFields: (keyof T)[] = []; for (const field of fieldsToCheck) { const a: any = (newData as any)[field]; const b: any = (oldData as any)[field]; const isEqual: boolean = comparators[field] ? comparators[field]!(a, b) : this._deepEqual(a, b); if (!isEqual) { changedFields.push(field); } } if (changedFields.length === 0) { return { result: 'equal' }; } return { result: 'modified', changedFields: changedFields }; } /** * Eldonti az osszehasonlitando fields-listat: * - explicit `options.fields` esetan annak masolata (skip-list ignoralt) * - egyebkent `Object.keys(newData) UNION Object.keys(oldData)` minus skip-list */ private _resolveCompareFields( newData: T, oldData: T, options?: DyNTS_CompareData_Options, ): (keyof T)[] { if (options?.fields !== undefined) { // Explicit fields — copy-spread (defensive against caller mutation mid-flight) return [...options.fields]; } const keySet: Set = new Set(); for (const k of Object.keys(newData as object)) { keySet.add(k); } for (const k of Object.keys(oldData as object)) { keySet.add(k); } const result: (keyof T)[] = []; for (const k of keySet) { if (COMPARE_DATA_METADATA_SKIP_FIELDS.has(k)) { continue; } result.push(k as keyof T); } return result; } /** * Deep-equal helper. * * - Primitive (`string`/`number`/`boolean`/`null`/`undefined`): `===` / `Object.is` * (a `NaN === NaN` esetet `Object.is` kezeli helyesen) * - `Date`: `.getTime()` egyenloseg (kulonbozo Date instance-ok same-time-mal egyenlonek) * - Array: length + index-szerinti rekurziv deep-equal * - POJO: keys-union + per-key rekurzio * - Egyeb (RegExp / Map / Set / Buffer): `Object.is` fallback (reference compare) * * NEM kezelt: cyclic references — a hivo objektum-grafja flat / fa kell legyen * (a tipikus FDP data-model esete; cycle eseten stack overflow lesz, ami ertelmes * jelzes a programozonak). */ private _deepEqual(a: any, b: any): boolean { if (a === b) { return true; } if (Object.is(a, b)) { return true; } // Null/undefined es a masik nem ugyanaz → false if (a === null || a === undefined || b === null || b === undefined) { return false; } // Date if (a instanceof Date && b instanceof Date) { return a.getTime() === b.getTime(); } if (a instanceof Date || b instanceof Date) { return false; } // Array if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) { return false; } for (let i: number = 0; i < a.length; i++) { if (!this._deepEqual(a[i], b[i])) { return false; } } return true; } if (Array.isArray(a) || Array.isArray(b)) { return false; } // POJO (Object.prototype) — keys union, per-key compare if (typeof a === 'object' && typeof b === 'object') { const keysA: string[] = Object.keys(a); const keysB: string[] = Object.keys(b); if (keysA.length !== keysB.length) { return false; } const keysAset: Set = new Set(keysA); for (const k of keysB) { if (!keysAset.has(k)) { return false; } } for (const k of keysA) { if (!this._deepEqual(a[k], b[k])) { return false; } } return true; } // Fallback (function, symbol, BigInt, stb.) — strict equality mar nem stimmelt return false; } }