@airoom/nextmin-node 1.4.6 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +48 -5
  2. package/dist/api/apiRouter.d.ts +10 -1
  3. package/dist/api/apiRouter.js +83 -22
  4. package/dist/api/router/mountBatchRoutes.d.ts +2 -0
  5. package/dist/api/router/mountBatchRoutes.js +90 -0
  6. package/dist/api/router/mountCrudRoutes.js +218 -223
  7. package/dist/api/router/mountFindRoutes.js +2 -49
  8. package/dist/api/router/mountSearchRoutes.js +10 -52
  9. package/dist/api/router/mountSearchRoutes_extended.js +7 -48
  10. package/dist/api/router/setupFileRoutes.js +41 -0
  11. package/dist/api/router/utils.d.ts +2 -0
  12. package/dist/api/router/utils.js +37 -8
  13. package/dist/cli.d.ts +1 -0
  14. package/dist/cli.js +83 -0
  15. package/dist/database/DatabaseAdapter.d.ts +8 -0
  16. package/dist/database/NMAdapter.d.ts +46 -0
  17. package/dist/database/NMAdapter.js +1243 -0
  18. package/dist/database/QueryEngine.d.ts +14 -0
  19. package/dist/database/QueryEngine.js +215 -0
  20. package/dist/database/utils.d.ts +2 -0
  21. package/dist/database/utils.js +21 -0
  22. package/dist/files/FileStorageAdapter.d.ts +5 -0
  23. package/dist/files/LocalFileStorageAdapter.d.ts +5 -0
  24. package/dist/files/LocalFileStorageAdapter.js +14 -0
  25. package/dist/files/S3FileStorageAdapter.d.ts +5 -0
  26. package/dist/files/S3FileStorageAdapter.js +14 -0
  27. package/dist/index.d.ts +4 -1
  28. package/dist/index.js +11 -5
  29. package/dist/models/BaseModel.d.ts +31 -2
  30. package/dist/models/BaseModel.js +33 -5
  31. package/dist/policy/authorize.js +95 -38
  32. package/dist/schemas/Users.json +66 -30
  33. package/dist/services/AggregateService.d.ts +8 -0
  34. package/dist/services/AggregateService.js +87 -0
  35. package/dist/services/RealtimeService.d.ts +20 -0
  36. package/dist/services/RealtimeService.js +93 -0
  37. package/dist/services/SchemaService.d.ts +3 -0
  38. package/dist/services/SchemaService.js +6 -2
  39. package/dist/utils/DefaultDataInitializer.js +10 -2
  40. package/dist/utils/Events.d.ts +34 -0
  41. package/dist/utils/Events.js +55 -0
  42. package/dist/utils/Logger.d.ts +2 -1
  43. package/dist/utils/Logger.js +20 -9
  44. package/dist/utils/QueryCache.d.ts +16 -0
  45. package/dist/utils/QueryCache.js +106 -0
  46. package/dist/utils/SchemaLoader.d.ts +6 -1
  47. package/dist/utils/SchemaLoader.js +49 -5
  48. package/package.json +19 -4
  49. package/dist/database/InMemoryAdapter.d.ts +0 -15
  50. package/dist/database/InMemoryAdapter.js +0 -71
  51. package/dist/database/MongoAdapter.d.ts +0 -52
  52. package/dist/database/MongoAdapter.js +0 -410
@@ -0,0 +1,1243 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.NMAdapter = void 0;
7
+ const typeorm_1 = require("typeorm");
8
+ const mongodb_1 = require("mongodb");
9
+ const Logger_1 = __importDefault(require("../utils/Logger"));
10
+ const utils_1 = require("../api/router/utils");
11
+ const utils_2 = require("./utils");
12
+ /**
13
+ * NMAdapter implements the DatabaseAdapter interface.
14
+ * It supports SQL databases (PostgreSQL, MySQL, MariaDB, SQLite, MSSQL) and MongoDB.
15
+ */
16
+ class NMAdapter {
17
+ constructor(options) {
18
+ this.options = options;
19
+ this.repositories = new Map();
20
+ this.entitySchemas = new Map();
21
+ this.registeredSchemas = {};
22
+ this.imageProxyOptions = {};
23
+ const isValidUrl = (url) => typeof url === 'string' && url.startsWith('http');
24
+ let imageProxy = options.imageProxy;
25
+ if (!imageProxy || Object.keys(imageProxy).length === 0) {
26
+ imageProxy = { proxyBase: '/images' };
27
+ }
28
+ if (imageProxy && imageProxy.proxyBase) {
29
+ this.imageProxyOptions = { ...imageProxy };
30
+ // Auto-detect originalBase if missing but s3/r2 config exists
31
+ if (!this.imageProxyOptions.originalBase) {
32
+ const s3 = options.s3 || {
33
+ bucket: process.env.S3_BUCKET,
34
+ region: process.env.S3_REGION,
35
+ endpoint: process.env.S3_ENDPOINT,
36
+ publicBaseUrl: process.env.S3_PUBLIC_BASE_URL
37
+ };
38
+ if (s3.publicBaseUrl) {
39
+ this.imageProxyOptions.originalBase = s3.publicBaseUrl.replace(/\/+$/, '');
40
+ }
41
+ else if (s3.bucket) {
42
+ if (isValidUrl(s3.endpoint)) {
43
+ this.imageProxyOptions.originalBase = `${s3.endpoint.replace(/\/+$/, '')}/${s3.bucket}`;
44
+ }
45
+ else if (s3.region) {
46
+ this.imageProxyOptions.originalBase = `https://${s3.bucket}.s3.${s3.region}.amazonaws.com`;
47
+ }
48
+ }
49
+ }
50
+ if (this.imageProxyOptions.originalBase && this.imageProxyOptions.proxyBase) {
51
+ Logger_1.default.info('NMAdapter', `Image proxy enabled: ${this.imageProxyOptions.originalBase} -> ${this.imageProxyOptions.proxyBase}`);
52
+ }
53
+ }
54
+ this.dataSource = new typeorm_1.DataSource({
55
+ ...options,
56
+ entities: [], // Will be populated dynamically
57
+ synchronize: options.type === 'mongodb' ? false : true, // Only for development; use migrations for production in a real-world app
58
+ logging: false,
59
+ });
60
+ }
61
+ async connect() {
62
+ await this.dataSource.initialize();
63
+ Logger_1.default.info('NMAdapter', `Connected to database: ${this.dataSource.options.type}`);
64
+ }
65
+ async disconnect() {
66
+ await this.dataSource.destroy();
67
+ Logger_1.default.warn('NMAdapter', 'Disconnected from database');
68
+ }
69
+ async registerSchemas(schemas) {
70
+ // 1) Update cumulative schema map BEFORE building entities
71
+ for (const [name, def] of Object.entries(schemas)) {
72
+ this.registeredSchemas[name.toLowerCase()] = def;
73
+ }
74
+ // 2) Define EntitySchemas dynamically
75
+ for (const [name, def] of Object.entries(schemas)) {
76
+ const entitySchema = this.buildEntitySchema(def);
77
+ this.entitySchemas.set(name.toLowerCase(), entitySchema);
78
+ }
79
+ // 2) Update DataSource with new entities
80
+ // Note: For dynamic entity updates in TypeORM, we must clear old metadatas
81
+ // and rebuild them to ensure synchronize() detects all changes correctly.
82
+ this.dataSource.options.entities = Array.from(this.entitySchemas.values());
83
+ // Re-initialize metadata
84
+ if (this.dataSource.entityMetadatas) {
85
+ this.dataSource.entityMetadatas.length = 0;
86
+ }
87
+ await this.dataSource.buildMetadatas();
88
+ // Synchronize schema (if applicable)
89
+ if (this.dataSource.options.synchronize) {
90
+ try {
91
+ Logger_1.default.info('NMAdapter', `Synchronizing ${Object.keys(schemas).length} schemas...`);
92
+ await this.dataSource.synchronize();
93
+ Logger_1.default.info('NMAdapter', 'Schema synchronization completed successfully');
94
+ }
95
+ catch (err) {
96
+ // Ignore "table already exists" errors during dynamic sync
97
+ // This often happens in SQLite when metadata rebuilds don't perfectly align with existing tables
98
+ if (err.message?.includes('already exists')) {
99
+ Logger_1.default.info('NMAdapter', `Synchronize notice: Table already exists, skipping create: ${err.message}`);
100
+ }
101
+ else {
102
+ Logger_1.default.error('NMAdapter', `Schema synchronization failed: ${err.message}`, err);
103
+ throw err; // Re-throw critical errors
104
+ }
105
+ }
106
+ }
107
+ // 3) Sync Indexes
108
+ for (const [name, def] of Object.entries(schemas)) {
109
+ const indexes = {};
110
+ for (const [key, attr] of Object.entries(def.attributes)) {
111
+ if (attr.index === true) {
112
+ indexes[key] = 1;
113
+ }
114
+ }
115
+ if (Object.keys(indexes).length > 0) {
116
+ await this.syncIndexes(name, indexes);
117
+ }
118
+ }
119
+ // 4) Cache repositories
120
+ for (const name of Object.keys(schemas)) {
121
+ const lcName = name.toLowerCase();
122
+ const schema = this.entitySchemas.get(lcName);
123
+ if (schema) {
124
+ this.repositories.set(lcName, this.dataSource.getRepository(schema));
125
+ }
126
+ }
127
+ }
128
+ async syncIndexes(modelName, spec) {
129
+ const repo = this.repositories.get(modelName.toLowerCase());
130
+ if (!repo)
131
+ return;
132
+ if (this.dataSource.options.type === 'mongodb') {
133
+ const mongoRepo = repo.manager.getMongoRepository(repo.metadata.name);
134
+ for (const field of Object.keys(spec)) {
135
+ try {
136
+ const mongoRepo = repo.manager.getMongoRepository(repo.metadata.name);
137
+ // Use TypeORM's built-in createCollectionIndex if available, or native
138
+ await mongoRepo.createCollectionIndex({ [field]: 1 }, { background: true });
139
+ Logger_1.default.info('NMAdapter', `Created index on ${modelName}.${field}`);
140
+ }
141
+ catch (err) {
142
+ Logger_1.default.warn('NMAdapter', `Failed to create index on ${modelName}.${field}: ${err}`);
143
+ }
144
+ }
145
+ }
146
+ else {
147
+ // For SQL, TypeORM handles basic indexing via EntitySchema.synchronize()
148
+ // which we already called in registerSchemas.
149
+ }
150
+ }
151
+ buildEntitySchema(def) {
152
+ const isMongo = this.dataSource.options.type === 'mongodb';
153
+ const columns = {
154
+ id: {
155
+ primary: true,
156
+ objectId: isMongo,
157
+ // For MongoDB we use name: '_id' to map the physical column
158
+ name: isMongo ? '_id' : undefined,
159
+ // For MongoDB we use string/ObjectId, for SQL we use int
160
+ type: isMongo ? 'objectId' : 'int',
161
+ generated: isMongo ? true : 'increment',
162
+ },
163
+ createdAt: {
164
+ type: 'datetime',
165
+ createDate: true,
166
+ default: () => 'CURRENT_TIMESTAMP',
167
+ nullable: true,
168
+ },
169
+ updatedAt: {
170
+ type: 'datetime',
171
+ updateDate: true,
172
+ default: () => 'CURRENT_TIMESTAMP',
173
+ nullable: true,
174
+ },
175
+ };
176
+ const indices = [];
177
+ for (const [key, attr] of Object.entries(def.attributes)) {
178
+ if (key === 'id' || key === '_id')
179
+ continue;
180
+ // CRITICAL: If this field is physically stored in a base collection,
181
+ // do NOT define a column for it in this child entity internally,
182
+ // UNLESS it is an OVERRIDE (not inherited) and not marked as 'safe' in the base.
183
+ if (def.extends && key !== 'baseId') {
184
+ const head = Array.isArray(attr) ? attr[0] : attr;
185
+ if (head?.inherited) {
186
+ continue; // Skip purely inherited fields; they live in the base record
187
+ }
188
+ // It's an override or child-specific field
189
+ const baseSchema = this.registeredSchemas[def.extends];
190
+ const baseAttr = baseSchema?.attributes?.[key];
191
+ const bHead = Array.isArray(baseAttr) ? baseAttr[0] : baseAttr;
192
+ if (bHead?.safe === true) {
193
+ continue; // Base model protects this field; skip column in child
194
+ }
195
+ }
196
+ const head = Array.isArray(attr) ? attr[0] : attr;
197
+ if (head?.type === 'virtual')
198
+ continue;
199
+ const typeORMType = this.mapType(attr);
200
+ const isUnique = attr.unique === true;
201
+ const isSparse = attr.sparse === true;
202
+ columns[key] = {
203
+ type: typeORMType, // Map type appropriately (e.g., 'varchar' for strings)
204
+ nullable: true,
205
+ unique: isUnique && (!isMongo || !isSparse), // Standard unique if not sparse mongo
206
+ default: attr.default,
207
+ index: attr.index === true,
208
+ };
209
+ // For MongoDB, if unique and sparse, we use an explicit index definition
210
+ // since column-level uniqueness doesn't support the 'sparse' option in TypeORM's EntitySchema for Mongo.
211
+ if (isMongo && isUnique && isSparse) {
212
+ indices.push({
213
+ name: `IDX_${def.modelName}_${key}_UNIQUE`,
214
+ columns: [key],
215
+ unique: true,
216
+ sparse: true,
217
+ });
218
+ }
219
+ if (typeORMType === 'datetime' && attr.default === 'now') {
220
+ columns[key].default = () => 'CURRENT_TIMESTAMP';
221
+ }
222
+ }
223
+ return new typeorm_1.EntitySchema({
224
+ name: def.modelName,
225
+ tableName: def.collection || def.modelName.toLowerCase(), // Reverted to lowercase for DB compatibility
226
+ columns,
227
+ indices: indices.length > 0 ? indices : undefined,
228
+ });
229
+ }
230
+ mapType(attr) {
231
+ if (Array.isArray(attr)) {
232
+ return 'simple-json';
233
+ }
234
+ const type = String(attr?.type || 'string').toLowerCase();
235
+ switch (type) {
236
+ case 'string': return 'varchar';
237
+ case 'number': return 'float';
238
+ case 'boolean': return 'boolean';
239
+ case 'date':
240
+ case 'datetime': return 'datetime';
241
+ case 'json':
242
+ case 'object': return 'json';
243
+ case 'array': return 'simple-json';
244
+ case 'objectid':
245
+ return 'varchar';
246
+ default: return 'varchar';
247
+ }
248
+ }
249
+ getRepository(collection) {
250
+ const repo = this.repositories.get(collection.toLowerCase());
251
+ if (!repo) {
252
+ throw new Error(`Repository for ${collection} not found. Did you register the schema?`);
253
+ }
254
+ return repo;
255
+ }
256
+ async create(collection, data, schemaDefinition) {
257
+ const repo = this.getRepository(collection);
258
+ const coerced = this.coercePayloadForStorage(data, schemaDefinition);
259
+ // Ensure timestamps are set if not provided (especially for MongoDB)
260
+ const now = new Date();
261
+ if (!coerced.createdAt)
262
+ coerced.createdAt = now;
263
+ if (!coerced.updatedAt)
264
+ coerced.updatedAt = now;
265
+ const created = await repo.save(coerced);
266
+ return this.prepareResult(created);
267
+ }
268
+ coercePayloadForStorage(payload, schema) {
269
+ if (!payload || !schema || this.dataSource.options.type !== 'mongodb')
270
+ return payload;
271
+ const coerced = { ...payload };
272
+ for (const [key, attr] of Object.entries(schema.attributes)) {
273
+ const a = Array.isArray(attr) ? attr[0] : attr;
274
+ const isObjectIdField = String(a?.type || '').toLowerCase() === 'objectid';
275
+ // For MongoDB, we remove null values to satisfy sparse indexes
276
+ if (coerced[key] === null) {
277
+ delete coerced[key];
278
+ continue;
279
+ }
280
+ if (isObjectIdField && coerced[key] != null) {
281
+ if (Array.isArray(coerced[key])) {
282
+ coerced[key] = coerced[key].map((v) => this.convertToObjectId(v));
283
+ }
284
+ else {
285
+ coerced[key] = this.convertToObjectId(coerced[key]);
286
+ }
287
+ }
288
+ }
289
+ return coerced;
290
+ }
291
+ async read(collection, query, limit, skip, schemaDefinition, includePrivateFields, options) {
292
+ const repo = this.getRepository(collection);
293
+ const isMongo = this.dataSource.options.type === 'mongodb';
294
+ const resolvedQuery = await this.resolveNestedSearches(query);
295
+ const where = this.transformQuery(resolvedQuery, schemaDefinition);
296
+ const findOptions = {
297
+ where,
298
+ take: limit,
299
+ skip: skip,
300
+ order: options?.sort,
301
+ };
302
+ if (options?.projection) {
303
+ const hasRootProj = this.isExclusiveProjection(options.projection, schemaDefinition);
304
+ if (hasRootProj) {
305
+ const dbProjection = {};
306
+ // Always include ID for hydration purposes
307
+ if (isMongo)
308
+ dbProjection._id = true;
309
+ else
310
+ dbProjection.id = true;
311
+ for (const [key, value] of Object.entries(options.projection)) {
312
+ // Skip dynamic aggregate fields like model[count] for the DB query
313
+ if (key.includes('[') && key.endsWith(']')) {
314
+ continue;
315
+ }
316
+ if (key.includes('.') && schemaDefinition) {
317
+ const root = key.split('.')[0];
318
+ const attr = schemaDefinition.attributes[root];
319
+ const a = Array.isArray(attr) ? attr[0] : attr;
320
+ const isRelation = String(a?.type || '').toLowerCase() === 'objectid';
321
+ if (isRelation) {
322
+ // For relations, we only need the root field (the ID) in the DB query.
323
+ // Adding the dotted key (e.g. 'districts.name') causes a path collision in Mongo
324
+ // because 'districts' is just a list of ObjectIds in this collection.
325
+ dbProjection[root] = true;
326
+ continue;
327
+ }
328
+ }
329
+ dbProjection[key] = !!value;
330
+ }
331
+ findOptions.select = dbProjection;
332
+ if (isMongo) {
333
+ const mongoProj = {};
334
+ for (const [k, v] of Object.entries(dbProjection)) {
335
+ const tk = (k === 'id') ? '_id' : k;
336
+ mongoProj[tk] = v ? 1 : 0;
337
+ }
338
+ findOptions.projection = mongoProj;
339
+ }
340
+ }
341
+ }
342
+ let results;
343
+ if (isMongo && options?.sample) {
344
+ const mongoRepo = repo.manager.getMongoRepository(repo.metadata.name);
345
+ const aggregation = [{ $match: where }];
346
+ aggregation.push({ $sample: { size: options.sample } });
347
+ if (findOptions.projection)
348
+ aggregation.push({ $project: findOptions.projection });
349
+ results = await mongoRepo.aggregate(aggregation).toArray();
350
+ }
351
+ else if (isMongo && findOptions.projection) {
352
+ const mongoRepo = repo.manager.getMongoRepository(repo.metadata.name);
353
+ const cursor = mongoRepo.createCursor(where);
354
+ cursor.project(findOptions.projection);
355
+ if (limit)
356
+ cursor.limit(limit);
357
+ if (skip)
358
+ cursor.skip(skip);
359
+ if (options?.sort)
360
+ cursor.sort(options.sort);
361
+ results = await cursor.toArray();
362
+ }
363
+ else {
364
+ if (options?.sample && !isMongo) {
365
+ const qb = repo.createQueryBuilder('entity');
366
+ qb.where(where);
367
+ if (limit)
368
+ qb.take(limit);
369
+ if (skip)
370
+ qb.skip(skip);
371
+ const dbType = this.dataSource.options.type;
372
+ if (dbType === 'postgres' || dbType === 'sqlite') {
373
+ qb.orderBy('RANDOM()');
374
+ }
375
+ else if (dbType === 'mysql' || dbType === 'mariadb') {
376
+ qb.orderBy('RAND()');
377
+ }
378
+ else if (dbType === 'mssql') {
379
+ qb.orderBy('NEWID()');
380
+ }
381
+ results = await qb.getMany();
382
+ }
383
+ else {
384
+ results = await repo.find(findOptions);
385
+ }
386
+ }
387
+ const mappedResults = results.map(r => this.prepareResult(r));
388
+ if (schemaDefinition) {
389
+ await this.hydrateRelations(mappedResults, schemaDefinition, options?.depth ?? 0, options);
390
+ }
391
+ return mappedResults;
392
+ }
393
+ prepareResult(item) {
394
+ if (!item)
395
+ return item;
396
+ const isMongo = this.dataSource.options.type === 'mongodb';
397
+ if (isMongo) {
398
+ // For MongoDB, ensure id is set from _id consistently
399
+ const rawId = item._id || item.id;
400
+ if (rawId) {
401
+ item.id = this.idToString(rawId);
402
+ }
403
+ }
404
+ else {
405
+ if (item.id != null)
406
+ item.id = String(item.id);
407
+ }
408
+ // Apply image URL proxying/transformation if configured
409
+ if (this.imageProxyOptions.originalBase && this.imageProxyOptions.proxyBase) {
410
+ this.transformUrls(item);
411
+ }
412
+ return item;
413
+ }
414
+ transformUrls(obj) {
415
+ if (!obj || typeof obj !== 'object')
416
+ return;
417
+ const { originalBase, proxyBase } = this.imageProxyOptions;
418
+ if (!originalBase || !proxyBase)
419
+ return;
420
+ for (const key in obj) {
421
+ const val = obj[key];
422
+ if (typeof val === 'string') {
423
+ if (val.startsWith(originalBase)) {
424
+ obj[key] = val.replace(originalBase, proxyBase);
425
+ }
426
+ }
427
+ else if (Array.isArray(val)) {
428
+ for (let i = 0; i < val.length; i++) {
429
+ if (typeof val[i] === 'string' && val[i].startsWith(originalBase)) {
430
+ val[i] = val[i].replace(originalBase, proxyBase);
431
+ }
432
+ else if (val[i] && typeof val[i] === 'object') {
433
+ this.transformUrls(val[i]);
434
+ }
435
+ }
436
+ }
437
+ else if (val && typeof val === 'object') {
438
+ this.transformUrls(val);
439
+ }
440
+ }
441
+ }
442
+ idToString(val) {
443
+ if (val == null)
444
+ return '';
445
+ if (typeof val === 'string')
446
+ return val;
447
+ if (typeof val === 'number')
448
+ return String(val);
449
+ // Handle MongoDB ObjectId or similar objects with toHexString
450
+ if (typeof val.toHexString === 'function')
451
+ return val.toHexString();
452
+ if (typeof val.toString === 'function' && val.constructor.name === 'ObjectId')
453
+ return val.toString();
454
+ if (Buffer.isBuffer(val))
455
+ return val.toString('hex');
456
+ if (val.id)
457
+ return this.idToString(val.id);
458
+ if (val._id)
459
+ return this.idToString(val._id);
460
+ return String(val);
461
+ }
462
+ convertToObjectId(val) {
463
+ if (val == null)
464
+ return val;
465
+ // If it's an object (potentially hydrated relation), extract the ID
466
+ if (typeof val === 'object' && !Buffer.isBuffer(val)) {
467
+ if (val instanceof mongodb_1.ObjectId || val.constructor?.name === 'ObjectId')
468
+ return val;
469
+ const extractedId = val.id || val._id;
470
+ if (extractedId)
471
+ return this.convertToObjectId(extractedId);
472
+ }
473
+ try {
474
+ if (this.dataSource.options.type === 'mongodb') {
475
+ if (typeof val === 'string' && val.length === 24 && /^[0-9a-fA-F]{24}$/.test(val)) {
476
+ return new mongodb_1.ObjectId(val);
477
+ }
478
+ }
479
+ else {
480
+ const n = Number(val);
481
+ if (!isNaN(n) && String(val).trim() !== '')
482
+ return n;
483
+ }
484
+ return val;
485
+ }
486
+ catch {
487
+ return val;
488
+ }
489
+ }
490
+ discoverInverseRelation(targetModelName, sourceModelHint) {
491
+ const targetLc = targetModelName.toLowerCase();
492
+ const sourceLc = sourceModelHint.toLowerCase();
493
+ // 1. Find a schema that matches sourceModelHint (plural or singular)
494
+ let sourceSchema;
495
+ // Try exact match first
496
+ sourceSchema = this.registeredSchemas[sourceLc];
497
+ // Try common plural/singular variations
498
+ if (!sourceSchema) {
499
+ const variations = [
500
+ sourceLc + 's',
501
+ sourceLc + 'es',
502
+ sourceLc.endsWith('y') ? sourceLc.slice(0, -1) + 'ies' : sourceLc,
503
+ sourceLc.endsWith('ies') ? sourceLc.slice(0, -3) + 'y' : sourceLc,
504
+ sourceLc.endsWith('s') ? sourceLc.slice(0, -1) : sourceLc
505
+ ];
506
+ for (const v of variations) {
507
+ if (this.registeredSchemas[v]) {
508
+ sourceSchema = this.registeredSchemas[v];
509
+ break;
510
+ }
511
+ }
512
+ }
513
+ if (!sourceSchema)
514
+ return null;
515
+ // 2. Find which field in sourceSchema points to targetModelName
516
+ for (const [field, attr] of Object.entries(sourceSchema.attributes)) {
517
+ const a = Array.isArray(attr) ? attr[0] : attr;
518
+ if (a.ref?.toLowerCase() === targetLc) {
519
+ return { sourceModel: sourceSchema.modelName, foreignField: field };
520
+ }
521
+ }
522
+ return null;
523
+ }
524
+ async hydrateDynamicAggregates(results, schema, projection) {
525
+ const promises = [];
526
+ const idsToFetch = results.map(r => r.id).filter(v => v != null);
527
+ if (idsToFetch.length === 0)
528
+ return;
529
+ for (const [projKey, projVal] of Object.entries(projection)) {
530
+ if (projVal !== 1)
531
+ continue;
532
+ if (!projKey.includes('[') || !projKey.endsWith(']'))
533
+ continue;
534
+ const parts = projKey.split('[');
535
+ const modelHint = parts[0];
536
+ const op = parts[1].slice(0, -1);
537
+ if (op !== 'count')
538
+ continue;
539
+ const relation = this.discoverInverseRelation(schema.modelName, modelHint);
540
+ if (!relation) {
541
+ Logger_1.default.warn('NMAdapter', `No inverse relation found for ${modelHint} pointing to ${schema.modelName}`);
542
+ continue;
543
+ }
544
+ promises.push((async () => {
545
+ try {
546
+ const sourceSchema = this.registeredSchemas[relation.sourceModel.toLowerCase()];
547
+ const counts = await this.countBy(relation.sourceModel, relation.foreignField, { [relation.foreignField]: { $in: idsToFetch } }, sourceSchema);
548
+ const targetKey = `${modelHint}Count`;
549
+ results.forEach(r => {
550
+ const lid = this.idToString(r.id);
551
+ r[targetKey] = counts[lid] || 0;
552
+ });
553
+ }
554
+ catch (err) {
555
+ Logger_1.default.warn('NMAdapter', `Failed to fetch dynamic aggregate ${projKey} for ${schema.modelName}: ${err}`);
556
+ }
557
+ })());
558
+ }
559
+ if (promises.length > 0) {
560
+ await Promise.all(promises);
561
+ }
562
+ }
563
+ async hydrateRelations(results, schema, depth = 0, options) {
564
+ if (!results.length || depth > 2)
565
+ return;
566
+ const projection = options?.projection;
567
+ // --- Dynamic Aggregates (e.g. model[count]) ---
568
+ if (projection) {
569
+ await this.hydrateDynamicAggregates(results, schema, projection);
570
+ }
571
+ // 1) Handle inheritance: if schema extends another, fetch base records
572
+ if (schema.extends) {
573
+ const baseLc = schema.extends.toLowerCase();
574
+ const baseRepo = this.repositories.get(baseLc);
575
+ if (baseRepo) {
576
+ const baseIds = new Set();
577
+ for (const item of results) {
578
+ if (item.baseId != null) {
579
+ baseIds.add(String(item.baseId));
580
+ }
581
+ }
582
+ if (baseIds.size > 0) {
583
+ const baseSchema = this.registeredSchemas[baseLc];
584
+ const baseAttrKeys = new Set(Object.keys(baseSchema.attributes || {}));
585
+ const hasRootProj = this.isExclusiveProjection(projection, baseSchema);
586
+ let baseProjection = undefined;
587
+ if (hasRootProj && projection) {
588
+ baseProjection = { id: 1 };
589
+ for (const [pk, pv] of Object.entries(projection)) {
590
+ if (baseAttrKeys.has(pk)) {
591
+ baseProjection[pk] = pv;
592
+ }
593
+ }
594
+ }
595
+ const baseDocs = await this.read(schema.extends, { id: { $in: Array.from(baseIds) } }, baseIds.size, 0, baseSchema, true, { projection: baseProjection });
596
+ const baseMap = new Map();
597
+ baseDocs.forEach(b => {
598
+ const key = String(b.id);
599
+ baseMap.set(key, b);
600
+ });
601
+ for (let i = 0; i < results.length; i++) {
602
+ const item = results[i];
603
+ if (item.baseId != null) {
604
+ const bid = String(item.baseId);
605
+ const baseData = baseMap.get(bid);
606
+ if (baseData) {
607
+ // Intelligent Merge: Prefer child data but skip nulls/empty for inherited or common fields
608
+ const result = { ...baseData };
609
+ for (const k of Object.keys(item)) {
610
+ const val = item[k];
611
+ const hasValue = val != null && (!Array.isArray(val) || val.length > 0);
612
+ if (hasValue) {
613
+ result[k] = val;
614
+ }
615
+ }
616
+ result.exId = String(baseData.id || baseData._id || '');
617
+ result.id = String(item.id || item._id || '');
618
+ result._id = result.id;
619
+ if (hasRootProj && projection) {
620
+ for (const k of Object.keys(result)) {
621
+ if (k !== 'id' && k !== 'baseId' && !projection[k]) {
622
+ if (baseAttrKeys.has(k)) {
623
+ delete result[k];
624
+ }
625
+ }
626
+ }
627
+ }
628
+ results[i] = result;
629
+ }
630
+ }
631
+ }
632
+ }
633
+ }
634
+ }
635
+ // 2) Handle standard ObjectId hydration & 3) json-group in parallel
636
+ const hydrationPromises = [];
637
+ // --- Virtual fields (Inverse relationship counts) ---
638
+ const virtualFields = Object.entries(schema.attributes).filter(([key, attr]) => {
639
+ const a = Array.isArray(attr) ? attr[0] : attr;
640
+ return a?.type === 'virtual' && a?.op === 'count';
641
+ });
642
+ for (const [key, attr] of virtualFields) {
643
+ hydrationPromises.push((async () => {
644
+ const a = Array.isArray(attr) ? attr[0] : attr;
645
+ const refModel = a.ref;
646
+ const foreignField = a.foreignField;
647
+ const localField = a.localField || 'id';
648
+ if (!refModel || !foreignField)
649
+ return;
650
+ const idsToFetch = results.map(r => r[localField]).filter(v => v != null);
651
+ if (idsToFetch.length === 0)
652
+ return;
653
+ const counts = await this.countBy(refModel, foreignField, { [foreignField]: { $in: idsToFetch } });
654
+ results.forEach(r => {
655
+ const lid = this.idToString(r[localField]);
656
+ r[key] = counts[lid] || 0;
657
+ });
658
+ })());
659
+ }
660
+ // --- Standard Relations ---
661
+ const relFields = Object.entries(schema.attributes).filter(([key, attr]) => {
662
+ if (key === 'baseId')
663
+ return false;
664
+ const a = Array.isArray(attr) ? attr[0] : attr;
665
+ return String(a?.type).toLowerCase() === 'objectid' && a?.ref;
666
+ });
667
+ for (const [key, attr] of relFields) {
668
+ hydrationPromises.push((async () => {
669
+ const hasRootProj = this.isExclusiveProjection(projection, schema);
670
+ const isTargeted = projection && (projection[key] || Object.keys(projection).some(pk => pk.startsWith(`${key}.`)));
671
+ if (projection && hasRootProj && !isTargeted)
672
+ return;
673
+ const isArray = Array.isArray(attr);
674
+ const refModelName = (isArray ? attr[0] : attr).ref;
675
+ if (!refModelName)
676
+ return;
677
+ const refLc = refModelName.toLowerCase();
678
+ const refRepo = this.repositories.get(refLc);
679
+ if (!refRepo)
680
+ return;
681
+ const idsToFetch = new Set();
682
+ for (const item of results) {
683
+ const val = item[key];
684
+ if (val == null)
685
+ continue;
686
+ if (isArray && Array.isArray(val)) {
687
+ val.forEach(v => { if (v != null)
688
+ idsToFetch.add(v); });
689
+ }
690
+ else {
691
+ idsToFetch.add(val);
692
+ }
693
+ }
694
+ if (idsToFetch.size === 0)
695
+ return;
696
+ let subProjection = options?._populate?.[key];
697
+ if (subProjection) {
698
+ subProjection = { ...subProjection, id: 1 };
699
+ }
700
+ if (!subProjection && projection) {
701
+ const prefix = `${key}.`;
702
+ for (const [pk, pv] of Object.entries(projection)) {
703
+ if (pk.startsWith(prefix)) {
704
+ if (!subProjection)
705
+ subProjection = { id: 1 };
706
+ subProjection[pk.slice(prefix.length)] = pv;
707
+ }
708
+ }
709
+ }
710
+ const fetchedDocs = await this.read(refModelName, { id: { $in: Array.from(idsToFetch) } }, idsToFetch.size, 0, this.registeredSchemas[refModelName], true, {
711
+ depth: depth + 1,
712
+ projection: subProjection
713
+ });
714
+ const docMap = new Map();
715
+ fetchedDocs.forEach(d => {
716
+ const sid = this.idToString(d.id);
717
+ if (sid)
718
+ docMap.set(sid, d);
719
+ });
720
+ for (const item of results) {
721
+ const val = item[key];
722
+ if (val == null)
723
+ continue;
724
+ if (Array.isArray(val)) {
725
+ item[key] = val.map(v => docMap.get(this.idToString(v)) || v);
726
+ }
727
+ else {
728
+ item[key] = docMap.get(this.idToString(val)) || val;
729
+ }
730
+ }
731
+ })());
732
+ }
733
+ // --- JSON groups ---
734
+ const jsonGroupFields = Object.entries(schema.attributes).filter(([key, attr]) => {
735
+ const a = Array.isArray(attr) ? attr[0] : attr;
736
+ return a?.format === 'json-group' && a?.attributes;
737
+ });
738
+ for (const [key, attr] of jsonGroupFields) {
739
+ hydrationPromises.push((async () => {
740
+ const a = Array.isArray(attr) ? attr[0] : attr;
741
+ const nestedSchema = {
742
+ modelName: `${schema.modelName}_${key}`,
743
+ attributes: a.attributes,
744
+ allowedMethods: {},
745
+ showCount: false
746
+ };
747
+ for (const item of results) {
748
+ let val = item[key];
749
+ if (!val)
750
+ continue;
751
+ if (typeof val === 'string' && val.trim()) {
752
+ try {
753
+ val = JSON.parse(val);
754
+ }
755
+ catch {
756
+ continue;
757
+ }
758
+ }
759
+ if (val && (typeof val === 'object' || Array.isArray(val))) {
760
+ const nestedResults = Array.isArray(val) ? val : [val];
761
+ await this.hydrateRelations(nestedResults, nestedSchema, depth + 1);
762
+ item[key] = val;
763
+ }
764
+ }
765
+ })());
766
+ }
767
+ await Promise.all(hydrationPromises);
768
+ }
769
+ async update(collection, targetId, data, schemaDefinition) {
770
+ if (targetId == null || targetId === '' || targetId === 'undefined' || targetId === 'null') {
771
+ throw new Error(`[NMAdapter] Cannot perform update on '${collection}': targetId is required and cannot be null/undefined. This guard prevents accidental bulk updates.`);
772
+ }
773
+ const repo = this.repositories.get(collection.toLowerCase());
774
+ if (!repo)
775
+ throw new Error(`Repository for ${collection} not found.`);
776
+ const isMongo = this.dataSource.options.type === 'mongodb';
777
+ const resolvedTargetId = isMongo ? this.convertToObjectId(targetId) : targetId;
778
+ const coerced = this.coercePayloadForStorage(data, schemaDefinition);
779
+ // Strip ID fields from data to prevent primary key conflicts
780
+ const { id: _unusedId, _id: _unusedRawId, ...updateData } = coerced;
781
+ const now = new Date();
782
+ updateData.updatedAt = now;
783
+ if (isMongo) {
784
+ // Using explicit getMongoRepository and updateOne prevents accidental bulk updates
785
+ // that targetBy filter {} could cause in standard TypeORM .update()
786
+ const mongoRepo = this.dataSource.getMongoRepository(repo.metadata.name);
787
+ const setObj = {};
788
+ const unsetObj = {};
789
+ // We look at the original data for nulls because coercePayloadForStorage strips them
790
+ for (const [k, v] of Object.entries(data)) {
791
+ if (k === 'id' || k === '_id')
792
+ continue;
793
+ if (v === null) {
794
+ unsetObj[k] = "";
795
+ }
796
+ else {
797
+ // Use the coerced value if it exists (for ObjectIds)
798
+ setObj[k] = updateData[k] !== undefined ? updateData[k] : v;
799
+ }
800
+ }
801
+ // Always update the updatedAt field
802
+ setObj.updatedAt = now;
803
+ const updatePayload = {};
804
+ if (Object.keys(setObj).length > 0)
805
+ updatePayload.$set = setObj;
806
+ if (Object.keys(unsetObj).length > 0)
807
+ updatePayload.$unset = unsetObj;
808
+ if (Object.keys(updatePayload).length > 0) {
809
+ await mongoRepo.updateOne({ _id: resolvedTargetId }, updatePayload);
810
+ }
811
+ }
812
+ else {
813
+ await repo.update(resolvedTargetId, updateData);
814
+ }
815
+ const updated = await this.read(collection, { id: resolvedTargetId }, 1, 0, schemaDefinition);
816
+ return updated[0];
817
+ }
818
+ async delete(collection, targetId, schemaDefinition) {
819
+ if (targetId == null || targetId === '' || targetId === 'undefined' || targetId === 'null') {
820
+ throw new Error(`[NMAdapter] Cannot perform delete on '${collection}': targetId is required and cannot be null/undefined. This guard prevents accidental bulk deletions.`);
821
+ }
822
+ const repo = this.repositories.get(collection.toLowerCase());
823
+ if (!repo)
824
+ throw new Error(`Repository for ${collection} not found.`);
825
+ const isMongo = this.dataSource.options.type === 'mongodb';
826
+ // Use a string ID for the initial read to avoid any ObjectId conversion issues in transformQuery
827
+ const idStr = this.idToString(targetId);
828
+ const existing = await this.read(collection, { id: idStr }, 1, 0, schemaDefinition);
829
+ const id = isMongo ? this.convertToObjectId(targetId) : targetId;
830
+ if (isMongo) {
831
+ const item = await repo.findOneBy({ _id: id });
832
+ if (item)
833
+ await repo.remove(item);
834
+ }
835
+ else {
836
+ await repo.delete(id);
837
+ }
838
+ return existing[0] || null;
839
+ }
840
+ async count(collection, query, schemaDefinition) {
841
+ const repo = this.getRepository(collection);
842
+ const resolvedQuery = await this.resolveNestedSearches(query);
843
+ const where = this.transformQuery(resolvedQuery, schemaDefinition);
844
+ let total;
845
+ if (this.dataSource.options.type === 'mongodb') {
846
+ total = await repo.countBy(where);
847
+ }
848
+ else {
849
+ total = await repo.count({ where });
850
+ }
851
+ return total;
852
+ }
853
+ async countBy(collection, field, query, schema) {
854
+ const repo = this.getRepository(collection);
855
+ const where = this.transformQuery(query, schema);
856
+ const counts = {};
857
+ if (this.dataSource.options.type === 'mongodb') {
858
+ const mongoRepo = repo.manager.getMongoRepository(repo.metadata.name);
859
+ // Determine if field is an array from schema to use $unwind
860
+ const isArray = Array.isArray(schema?.attributes?.[field]);
861
+ const pipeline = [{ $match: where }];
862
+ if (isArray) {
863
+ pipeline.push({ $unwind: `$${field}` });
864
+ }
865
+ pipeline.push({ $group: { _id: `$${field}`, count: { $sum: 1 } } });
866
+ const results = await mongoRepo.aggregate(pipeline).toArray();
867
+ results.forEach((r) => {
868
+ if (r._id != null)
869
+ counts[this.idToString(r._id)] = r.count;
870
+ });
871
+ }
872
+ else {
873
+ const results = await repo
874
+ .createQueryBuilder('entity')
875
+ .select(`entity.${field}`, 'key')
876
+ .addSelect('COUNT(*)', 'count')
877
+ .where(where)
878
+ .groupBy(`entity.${field}`)
879
+ .getRawMany();
880
+ results.forEach((r) => {
881
+ const k = r.key || r.KEY || r[`entity_${field}`];
882
+ if (k != null) {
883
+ counts[this.idToString(k)] = parseInt(r.count || r.COUNT || 0, 10);
884
+ }
885
+ });
886
+ }
887
+ return counts;
888
+ }
889
+ async findFirstRelatedIds(collection, field, ids) {
890
+ const repo = this.getRepository(collection);
891
+ const map = new Map();
892
+ if (this.dataSource.options.type === 'mongodb') {
893
+ const objectIds = ids.map(id => this.convertToObjectId(id));
894
+ const stringIds = ids.map(id => this.idToString(id));
895
+ const allIds = Array.from(new Set([...objectIds, ...stringIds]));
896
+ const results = await repo.manager.getMongoRepository(repo.metadata.name).aggregate([
897
+ { $match: { [field]: { $in: allIds } } },
898
+ { $group: { _id: `$${field}`, firstId: { $first: '$_id' } } }
899
+ ]).toArray();
900
+ results.forEach(r => {
901
+ const parentId = this.idToString(r._id);
902
+ const firstId = this.idToString(r.firstId);
903
+ if (parentId && firstId) {
904
+ map.set(parentId, firstId);
905
+ }
906
+ });
907
+ }
908
+ else {
909
+ // SQL: Use GROUP BY to find at least one related record per parent ID
910
+ const qb = repo.createQueryBuilder('entity');
911
+ const results = await qb
912
+ .select([`entity.${field}`, 'MIN(entity.id) as firstId'])
913
+ .where(`entity.${field} IN (:...ids)`, { ids })
914
+ .groupBy(`entity.${field}`)
915
+ .getRawMany();
916
+ results.forEach(r => {
917
+ // Handle various raw column name formats from different SQL drivers
918
+ const parentId = r[`entity_${field}`] || r[field] || r[`entity_${field.toLowerCase()}`];
919
+ const relId = r.firstId || r.firstid || r.FIRSTID;
920
+ if (parentId != null && relId != null) {
921
+ map.set(this.idToString(parentId), this.idToString(relId));
922
+ }
923
+ });
924
+ }
925
+ return map;
926
+ }
927
+ /**
928
+ * Centralized Nested Search Resolver
929
+ * Recursively scans for $nestedSearch operators and resolves them to { $in: [ids] }
930
+ */
931
+ async resolveNestedSearches(query) {
932
+ if (!(0, utils_1.isPlainObject)(query))
933
+ return query;
934
+ const resolved = { ...query };
935
+ for (const [key, value] of Object.entries(resolved)) {
936
+ if ((key === '$or' || key === '$and') && Array.isArray(value)) {
937
+ resolved[key] = await Promise.all(value.map(v => this.resolveNestedSearches(v)));
938
+ continue;
939
+ }
940
+ if ((0, utils_1.isPlainObject)(value)) {
941
+ const opObj = value;
942
+ if (opObj.$nestedSearch) {
943
+ const { ref, searchBy, show, value: searchVal, operator = '$in' } = opObj.$nestedSearch;
944
+ const refSchema = Object.values(this.registeredSchemas).find(s => s.modelName.toLowerCase() === ref.toLowerCase());
945
+ if (!refSchema) {
946
+ Logger_1.default.warn('NMAdapter', `Schema not found for nested search ref: ${ref}`);
947
+ delete resolved[key];
948
+ continue;
949
+ }
950
+ // 1) Collect all unique fields to search in the referenced model
951
+ const searchFields = new Set();
952
+ if (show)
953
+ searchFields.add(show);
954
+ if (searchBy) {
955
+ if (Array.isArray(searchBy)) {
956
+ searchBy.forEach((f) => searchFields.add(f));
957
+ }
958
+ else {
959
+ searchFields.add(searchBy);
960
+ }
961
+ }
962
+ if (searchFields.size === 0)
963
+ searchFields.add('name'); // fallback
964
+ // 2) Build conditions for secondary search
965
+ const conditions = [];
966
+ const isArrayVal = Array.isArray(searchVal);
967
+ for (const field of Array.from(searchFields)) {
968
+ if (isArrayVal) {
969
+ conditions.push({ [field]: { $in: searchVal } });
970
+ }
971
+ else {
972
+ const anchoredValue = String(searchVal).startsWith('^') ? searchVal : `^${searchVal}`;
973
+ if (field.includes('.')) {
974
+ // Support deep nested search in relations (e.g. "districts.name")
975
+ const [localKey, ...rest] = field.split('.');
976
+ const attr = refSchema.attributes[localKey];
977
+ const a = Array.isArray(attr) ? attr[0] : attr;
978
+ if (a?.ref) {
979
+ // It's another relation! Delegate via another $nestedSearch
980
+ conditions.push({
981
+ [localKey]: {
982
+ $nestedSearch: {
983
+ ref: a.ref,
984
+ show: rest.join('.'),
985
+ value: anchoredValue
986
+ }
987
+ }
988
+ });
989
+ }
990
+ else {
991
+ // Just a nested object field
992
+ conditions.push({ [field]: { $regex: anchoredValue, $options: 'i' } });
993
+ }
994
+ }
995
+ else {
996
+ conditions.push({ [field]: { $regex: anchoredValue, $options: 'i' } });
997
+ }
998
+ }
999
+ }
1000
+ // 2.1) ADDITION: If it looks like a 24-char hex string (ObjectId), also search by ID directly
1001
+ if (!isArrayVal && typeof searchVal === 'string' && /^[0-9a-fA-F]{24}$/.test(searchVal)) {
1002
+ conditions.push({ id: searchVal });
1003
+ }
1004
+ // 3) Use this.read() to find matching IDs in the referenced collection
1005
+ const foundRelDocs = await this.read(ref, { $or: conditions }, 2000, 0, refSchema, true);
1006
+ const ids = foundRelDocs.map(d => String(d.id || d._id)).filter(Boolean);
1007
+ if (operator === '$nin') {
1008
+ resolved[key] = { $nin: ids };
1009
+ }
1010
+ else if (ids.length === 1) {
1011
+ resolved[key] = ids[0];
1012
+ }
1013
+ else if (ids.length > 1) {
1014
+ resolved[key] = { $in: ids };
1015
+ }
1016
+ else {
1017
+ // Unmatchable ID to ensure result is empty for non-$nin
1018
+ resolved[key] = { $in: [this.convertToObjectId('000000000000000000000000')] };
1019
+ }
1020
+ }
1021
+ else {
1022
+ resolved[key] = await this.resolveNestedSearches(value);
1023
+ }
1024
+ }
1025
+ }
1026
+ return resolved;
1027
+ }
1028
+ transformQuery(query, schema) {
1029
+ if (!(0, utils_1.isPlainObject)(query))
1030
+ return query;
1031
+ const isMongo = this.dataSource.options.type === 'mongodb';
1032
+ const transformed = {};
1033
+ for (const [key, value] of Object.entries(query)) {
1034
+ // Map both 'id' and '_id' to '_id' for MongoDB queries
1035
+ const targetKey = (isMongo && (key === 'id' || key === '_id')) ? '_id' : key;
1036
+ const attr = schema?.attributes?.[key];
1037
+ const a = Array.isArray(attr) ? attr[0] : attr;
1038
+ // id and any field with type ObjectId are treated as ID fields
1039
+ const isObjectIdField = key === 'id' || key === '_id' || (a && String(a.type).toLowerCase() === 'objectid');
1040
+ if ((key === '$or' || key === '$and') && Array.isArray(value)) {
1041
+ const subQueries = value.map(v => this.transformQuery(v, schema));
1042
+ transformed[key] = subQueries;
1043
+ continue;
1044
+ }
1045
+ if ((0, utils_1.isPlainObject)(value)) {
1046
+ const keys = Object.keys(value);
1047
+ if (keys.some(k => k.startsWith('$'))) {
1048
+ transformed[targetKey] = this.mapOperator(value, isMongo && isObjectIdField);
1049
+ }
1050
+ else {
1051
+ transformed[targetKey] = this.transformQuery(value, schema);
1052
+ }
1053
+ }
1054
+ else {
1055
+ if (isObjectIdField) {
1056
+ if (isMongo) {
1057
+ if (Array.isArray(value)) {
1058
+ transformed[targetKey] = { $in: value.map(v => this.convertToObjectId(v)) };
1059
+ }
1060
+ else {
1061
+ transformed[targetKey] = this.convertToObjectId(value);
1062
+ }
1063
+ }
1064
+ else {
1065
+ // Coerce to number for SQL if it is a string numeric
1066
+ if (Array.isArray(value)) {
1067
+ transformed[targetKey] = (0, typeorm_1.In)(value.map(v => {
1068
+ const n = Number(v);
1069
+ return !isNaN(n) ? n : v;
1070
+ }));
1071
+ }
1072
+ else {
1073
+ const num = Number(value);
1074
+ transformed[targetKey] = !isNaN(num) ? num : value;
1075
+ }
1076
+ }
1077
+ }
1078
+ else {
1079
+ // Coerce string numeric values for SQL, but keep as-is for Mongo (integrity for string fields)
1080
+ if (!isMongo) {
1081
+ const num = Number(value);
1082
+ transformed[key] = !isNaN(num) ? num : value;
1083
+ }
1084
+ else {
1085
+ transformed[key] = value;
1086
+ }
1087
+ }
1088
+ }
1089
+ }
1090
+ // Special handling for top-level $or in SQL (TypeORM expects array for OR, but our $or needs to be flattened/merged if combined with other ANDs)
1091
+ if (!isMongo && transformed.$or && Array.isArray(transformed.$or)) {
1092
+ const orPart = transformed.$or;
1093
+ const otherPart = { ...transformed };
1094
+ delete otherPart.$or;
1095
+ if (Object.keys(otherPart).length === 0) {
1096
+ return orPart;
1097
+ }
1098
+ else {
1099
+ // Merge other filters into each OR condition: (A AND (B OR C)) -> (A AND B) OR (A AND C)
1100
+ return orPart.map((sub) => ({ ...otherPart, ...sub }));
1101
+ }
1102
+ }
1103
+ return transformed;
1104
+ }
1105
+ mapOperator(opObj, isObjectIdField = false) {
1106
+ const isMongo = this.dataSource.options.type === 'mongodb';
1107
+ if (opObj.$regex) {
1108
+ const val = String(opObj.$regex);
1109
+ const isCaseInsensitive = opObj.$options?.includes('i');
1110
+ if (isMongo) {
1111
+ return { $regex: val, $options: opObj.$options || '' };
1112
+ }
1113
+ let pattern = val;
1114
+ if (pattern.startsWith('^')) {
1115
+ pattern = pattern.slice(1);
1116
+ }
1117
+ else {
1118
+ pattern = `%${pattern}`;
1119
+ }
1120
+ if (pattern.endsWith('$')) {
1121
+ pattern = pattern.slice(0, -1);
1122
+ }
1123
+ else {
1124
+ pattern = `${pattern}%`;
1125
+ }
1126
+ return isCaseInsensitive ? (0, typeorm_1.ILike)(pattern) : (0, typeorm_1.Like)(pattern);
1127
+ }
1128
+ if (opObj.$gt !== undefined) {
1129
+ const val = isObjectIdField ? this.convertToObjectId(opObj.$gt) : opObj.$gt;
1130
+ return isMongo ? { $gt: val } : (0, typeorm_1.MoreThan)(val);
1131
+ }
1132
+ if (opObj.$gte !== undefined) {
1133
+ const val = isObjectIdField ? this.convertToObjectId(opObj.$gte) : opObj.$gte;
1134
+ return isMongo ? { $gte: val } : (0, typeorm_1.MoreThanOrEqual)(val);
1135
+ }
1136
+ if (opObj.$lt !== undefined) {
1137
+ const val = isObjectIdField ? this.convertToObjectId(opObj.$lt) : opObj.$lt;
1138
+ return isMongo ? { $lt: val } : (0, typeorm_1.LessThan)(val);
1139
+ }
1140
+ if (opObj.$lte !== undefined) {
1141
+ const val = isObjectIdField ? this.convertToObjectId(opObj.$lte) : opObj.$lte;
1142
+ return isMongo ? { $lte: val } : (0, typeorm_1.LessThanOrEqual)(val);
1143
+ }
1144
+ if (opObj.$in !== undefined) {
1145
+ const vals = Array.isArray(opObj.$in) ? opObj.$in : [opObj.$in];
1146
+ const mappedVals = isObjectIdField ? vals.map((v) => this.convertToObjectId(v)) : vals;
1147
+ return isMongo ? { $in: mappedVals } : (0, typeorm_1.In)(mappedVals);
1148
+ }
1149
+ if (opObj.$nin !== undefined) {
1150
+ const vals = Array.isArray(opObj.$nin) ? opObj.$nin : [opObj.$nin];
1151
+ const mappedVals = isObjectIdField ? vals.map((v) => this.convertToObjectId(v)) : vals;
1152
+ return isMongo ? { $nin: mappedVals } : (0, typeorm_1.Not)((0, typeorm_1.In)(mappedVals));
1153
+ }
1154
+ if (opObj.$ne !== undefined) {
1155
+ const val = isObjectIdField ? this.convertToObjectId(opObj.$ne) : opObj.$ne;
1156
+ return isMongo ? { $ne: val } : (0, typeorm_1.Not)((0, typeorm_1.Equal)(val));
1157
+ }
1158
+ if (opObj.$eq !== undefined) {
1159
+ const val = isObjectIdField ? this.convertToObjectId(opObj.$eq) : opObj.$eq;
1160
+ return isMongo ? val : (0, typeorm_1.Equal)(val);
1161
+ }
1162
+ return opObj;
1163
+ }
1164
+ async cleanupUnusedFields(schemas) {
1165
+ const report = {};
1166
+ const isMongo = this.dataSource.options.type === 'mongodb';
1167
+ if (isMongo) {
1168
+ for (const def of Object.values(schemas)) {
1169
+ const modelName = def.modelName;
1170
+ const repo = this.dataSource.getMongoRepository(modelName);
1171
+ // For MongoDB, we use the aggregation strategy to find all fields currently in the collection
1172
+ const keysPipeline = [
1173
+ { $project: { data: { $objectToArray: '$$ROOT' } } },
1174
+ { $unwind: '$data' },
1175
+ { $group: { _id: null, allKeys: { $addToSet: '$data.k' } } },
1176
+ ];
1177
+ const keysResult = await repo.aggregate(keysPipeline).toArray();
1178
+ const allKeys = keysResult[0]?.allKeys || [];
1179
+ const schemaKeys = new Set([
1180
+ ...Object.keys(def.attributes),
1181
+ '_id',
1182
+ '__v',
1183
+ 'createdAt',
1184
+ 'updatedAt',
1185
+ 'id',
1186
+ ]);
1187
+ const unusedKeys = allKeys.filter((k) => !schemaKeys.has(k));
1188
+ if (unusedKeys.length > 0) {
1189
+ const unsetObj = {};
1190
+ for (const k of unusedKeys) {
1191
+ unsetObj[k] = '';
1192
+ }
1193
+ await repo.updateMany({}, { $unset: unsetObj });
1194
+ report[modelName] = unusedKeys;
1195
+ Logger_1.default.info('NMAdapter', `Cleaned up ${unusedKeys.length} fields in Mongo collection ${modelName}: ${unusedKeys.join(', ')}`);
1196
+ }
1197
+ }
1198
+ return report;
1199
+ }
1200
+ // Default SQL Logic
1201
+ const queryRunner = this.dataSource.createQueryRunner();
1202
+ await queryRunner.connect();
1203
+ try {
1204
+ for (const def of Object.values(schemas)) {
1205
+ const modelName = def.modelName;
1206
+ const tableName = def.collection || modelName.toLowerCase();
1207
+ const table = await queryRunner.getTable(tableName);
1208
+ if (!table)
1209
+ continue;
1210
+ const schemaKeys = new Set([
1211
+ ...Object.keys(def.attributes),
1212
+ 'id',
1213
+ '_id',
1214
+ 'createdAt',
1215
+ 'updatedAt',
1216
+ ]);
1217
+ const columnsToDrop = table.columns
1218
+ .filter((col) => !schemaKeys.has(col.name))
1219
+ .map((col) => col.name);
1220
+ if (columnsToDrop.length > 0) {
1221
+ Logger_1.default.info('NMAdapter', `Dropping ${columnsToDrop.length} columns in ${tableName}: ${columnsToDrop.join(', ')}`);
1222
+ for (const colName of columnsToDrop) {
1223
+ try {
1224
+ await queryRunner.dropColumn(tableName, colName);
1225
+ }
1226
+ catch (err) {
1227
+ Logger_1.default.warn('NMAdapter', `Failed to drop column ${colName} in ${tableName}: ${err.message}`);
1228
+ }
1229
+ }
1230
+ report[modelName] = columnsToDrop;
1231
+ }
1232
+ }
1233
+ }
1234
+ finally {
1235
+ await queryRunner.release();
1236
+ }
1237
+ return report;
1238
+ }
1239
+ isExclusiveProjection(projection, schema) {
1240
+ return (0, utils_2.isExclusiveProjection)(projection, schema);
1241
+ }
1242
+ }
1243
+ exports.NMAdapter = NMAdapter;