@airoom/nextmin-node 2.0.1 → 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.
@@ -6,6 +6,10 @@ export interface APIRouterOptions {
6
6
  dbAdapter: DatabaseAdapter;
7
7
  server?: HttpServer;
8
8
  fileStorageAdapter?: FileStorageAdapter;
9
+ imageProxy?: {
10
+ originalBase?: string;
11
+ proxyBase?: string;
12
+ };
9
13
  }
10
14
  export declare class APIRouter {
11
15
  private router;
@@ -14,9 +18,10 @@ export declare class APIRouter {
14
18
  private jwtSecret;
15
19
  private trustedApiKey;
16
20
  private schemaLoader;
17
- private isDevelopment;
21
+ private get isDevelopment();
18
22
  private findRoutesMounted;
19
23
  private searchRoutesMounted;
24
+ private batchRoutesMounted;
20
25
  private authRoutesInitialized;
21
26
  private schemasRouteRegistered;
22
27
  private registeredModels;
@@ -25,6 +30,7 @@ export declare class APIRouter {
25
30
  private fileStorage?;
26
31
  private realtimeService?;
27
32
  private fileRoutesMounted;
33
+ private aggregateService?;
28
34
  constructor(options: APIRouterOptions);
29
35
  getRouter(): express.Router;
30
36
  private wireSchemaHotReload;
@@ -37,6 +43,7 @@ export declare class APIRouter {
37
43
  private mountRoutes;
38
44
  private mountFindRoutes;
39
45
  private mountSearchRoutes;
46
+ private mountBatchRoutes;
40
47
  private mountFileRoutes;
41
48
  private createCtx;
42
49
  private getSchema;
@@ -12,16 +12,23 @@ const SchemaLoader_1 = require("../utils/SchemaLoader");
12
12
  const DefaultDataInitializer_1 = require("../utils/DefaultDataInitializer");
13
13
  const RealtimeService_1 = require("../services/RealtimeService");
14
14
  const Events_1 = require("../utils/Events");
15
+ const AggregateService_1 = require("../services/AggregateService");
15
16
  const setupFileRoutes_1 = require("./router/setupFileRoutes");
16
17
  const setupAuthRoutes_1 = require("./router/setupAuthRoutes");
17
18
  const mountCrudRoutes_1 = require("./router/mountCrudRoutes");
18
19
  const mountFindRoutes_1 = require("./router/mountFindRoutes");
19
20
  const mountSearchRoutes_1 = require("./router/mountSearchRoutes");
21
+ const mountBatchRoutes_1 = require("./router/mountBatchRoutes");
20
22
  class APIRouter {
23
+ get isDevelopment() {
24
+ return (process.env.APP_MODE?.toLowerCase() !== 'production' &&
25
+ process.env.NODE_ENV?.toLowerCase() !== 'production');
26
+ }
21
27
  constructor(options) {
22
28
  this.models = {};
23
29
  this.findRoutesMounted = false;
24
30
  this.searchRoutesMounted = false;
31
+ this.batchRoutesMounted = false;
25
32
  this.authRoutesInitialized = false;
26
33
  this.schemasRouteRegistered = false;
27
34
  this.registeredModels = new Set();
@@ -182,7 +189,6 @@ class APIRouter {
182
189
  }
183
190
  return missing;
184
191
  };
185
- this.isDevelopment = process.env.APP_MODE !== 'production';
186
192
  this.router = express_1.default.Router();
187
193
  this.fileStorage = options.fileStorageAdapter;
188
194
  this.dbAdapter = options.dbAdapter;
@@ -211,6 +217,7 @@ class APIRouter {
211
217
  this.mountRoutes(initialSchemas);
212
218
  this.mountFindRoutes();
213
219
  this.mountSearchRoutes();
220
+ this.mountBatchRoutes();
214
221
  this.mountFileRoutes();
215
222
  await this.syncAllIndexes(initialSchemas);
216
223
  const initializer = new DefaultDataInitializer_1.DefaultDataInitializer(this.dbAdapter, this.models);
@@ -224,6 +231,7 @@ class APIRouter {
224
231
  Logger_1.default.error('APIRouter', 'Failed to initialize default data', err);
225
232
  }
226
233
  this.setupNotFoundMiddleware();
234
+ this.aggregateService = new AggregateService_1.AggregateService(this.createCtx());
227
235
  this.wireSchemaHotReload();
228
236
  };
229
237
  if (typeof this.dbAdapter.registerSchemas === 'function') {
@@ -384,6 +392,13 @@ class APIRouter {
384
392
  (0, mountSearchRoutes_1.mountSearchRoutes)(this.createCtx());
385
393
  this.ensureNotFoundLast();
386
394
  }
395
+ mountBatchRoutes() {
396
+ if (this.batchRoutesMounted)
397
+ return;
398
+ this.batchRoutesMounted = true;
399
+ (0, mountBatchRoutes_1.mountBatchRoutes)(this.createCtx());
400
+ this.ensureNotFoundLast();
401
+ }
387
402
  mountFileRoutes() {
388
403
  if (this.fileRoutesMounted)
389
404
  return;
@@ -0,0 +1,2 @@
1
+ import type { RouterCtx } from './ctx';
2
+ export declare function mountBatchRoutes(ctx: RouterCtx): void;
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mountBatchRoutes = mountBatchRoutes;
4
+ const authorize_1 = require("../../policy/authorize");
5
+ const QueryEngine_1 = require("../../database/QueryEngine");
6
+ const utils_1 = require("./utils");
7
+ function mountBatchRoutes(ctx) {
8
+ const { router } = ctx;
9
+ router.post('/_batch', ctx.optionalAuthMiddleware, async (req, res) => {
10
+ try {
11
+ const batchRequest = req.body;
12
+ if (!batchRequest || typeof batchRequest !== 'object') {
13
+ return res.status(400).json({ error: true, message: 'Invalid batch request' });
14
+ }
15
+ const results = {};
16
+ for (const [key, config] of Object.entries(batchRequest)) {
17
+ try {
18
+ const { path, query: queryParams } = config;
19
+ if (!path) {
20
+ results[key] = { error: true, message: 'Missing path' };
21
+ continue;
22
+ }
23
+ // Path is usually something like "/doctors" or "/doctors?slug=... "
24
+ // We need to extract the model name
25
+ const modelMatch = path.match(/^\/([^/?]+)/);
26
+ if (!modelMatch) {
27
+ results[key] = { error: true, message: 'Invalid path' };
28
+ continue;
29
+ }
30
+ const modelName = modelMatch[1].toLowerCase();
31
+ const schema = ctx.getSchema(modelName);
32
+ const model = ctx.getModel(modelName);
33
+ // Prepare pseudo-request for parseQuery
34
+ const pseudoReq = {
35
+ query: queryParams || {},
36
+ user: req.user,
37
+ };
38
+ const { limit, page, skip, sort, projection, sample, _populate } = (0, utils_1.parseQuery)(pseudoReq);
39
+ const filter = QueryEngine_1.QueryEngine.parse(pseudoReq.query, schema);
40
+ const roleStr = req.user?.role;
41
+ const pctx = {
42
+ isAuthenticated: !!req.user,
43
+ role: typeof roleStr === 'string' ? roleStr.toLowerCase() : null,
44
+ userId: req.user?.id || req.user?._id || null,
45
+ isSuperadmin: roleStr?.toLowerCase() === 'superadmin',
46
+ apiKeyOk: true,
47
+ };
48
+ const rdec = (0, authorize_1.authorize)(modelName, 'read', { allowedMethods: schema.allowedMethods, access: schema.access }, pctx);
49
+ if (!rdec.allow) {
50
+ results[key] = { error: true, message: 'Forbidden' };
51
+ continue;
52
+ }
53
+ const finalFilter = { ...filter, ...(rdec.queryFilter || {}) };
54
+ // Handle countBy if present in query
55
+ const countBy = String(pseudoReq.query.countBy || '').trim();
56
+ if (countBy && schema.attributes[countBy]) {
57
+ const counts = await ctx.dbAdapter.countBy(schema.modelName, countBy, finalFilter, schema);
58
+ results[key] = { success: true, counts };
59
+ continue;
60
+ }
61
+ const totalRows = await model.count(finalFilter);
62
+ const rows = await model.read(finalFilter, limit, skip, !!rdec.exposePrivate, {
63
+ sort,
64
+ projection,
65
+ sample,
66
+ _populate,
67
+ });
68
+ const data = rdec.exposePrivate
69
+ ? (0, authorize_1.applyReadMaskMany)(rows, rdec.sensitiveMask)
70
+ : (0, authorize_1.applyReadMaskMany)(rows, rdec.readMask);
71
+ results[key] = {
72
+ success: true,
73
+ data,
74
+ pagination: { totalRows, page, limit },
75
+ };
76
+ }
77
+ catch (err) {
78
+ results[key] = { error: true, message: err.message || 'Error' };
79
+ }
80
+ }
81
+ return res.status(200).json({
82
+ success: true,
83
+ results,
84
+ });
85
+ }
86
+ catch (err) {
87
+ return res.status(500).json({ error: true, message: err.message || 'Batch execution failed' });
88
+ }
89
+ });
90
+ }
@@ -296,6 +296,14 @@ function mountCrudRoutes(ctx, modelNameLC) {
296
296
  return res.status(403).json({ error: true, message: 'forbidden' });
297
297
  }
298
298
  const finalFilter = (0, authorize_1.andFilter)(filter, rdec.queryFilter || {});
299
+ const countBy = String(req.query.countBy ?? '').trim();
300
+ if (countBy && schema.attributes[countBy]) {
301
+ const counts = await ctx.dbAdapter.countBy(schema.modelName, countBy, finalFilter, schema);
302
+ return res.status(200).json({
303
+ success: true,
304
+ counts,
305
+ });
306
+ }
299
307
  const isUsersModel = modelNameLC === 'user' ||
300
308
  modelNameLC === 'users' ||
301
309
  (schema?.modelName ?? '').toLowerCase() === 'users';
@@ -640,7 +648,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
640
648
  }
641
649
  let updatedChild = before;
642
650
  if (Object.keys(childUpd).length) {
643
- updatedChild = await model.update(req.params.id, childUpd);
651
+ updatedChild = await model.update(req.params.id, childUpd, false, before);
644
652
  }
645
653
  if (targetBaseId && Object.keys(baseUpd).length) {
646
654
  await baseModel.update(targetBaseId, baseUpd);
@@ -687,7 +695,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
687
695
  })),
688
696
  });
689
697
  }
690
- const updatedRecord = await model.update(req.params.id, upd);
698
+ const updatedRecord = await model.update(req.params.id, upd, false, before);
691
699
  let responseDoc = updatedRecord;
692
700
  if (udec.exposePrivate && updatedRecord?.id) {
693
701
  const refetched = await model.read({ id: updatedRecord.id }, 1, 0, true);
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.setupFileRoutes = setupFileRoutes;
7
7
  const multer_1 = __importDefault(require("multer"));
8
+ const Logger_1 = __importDefault(require("../../utils/Logger"));
8
9
  function setupFileRoutes(ctx) {
9
10
  if (!ctx.fileStorage)
10
11
  return;
@@ -53,6 +54,46 @@ function setupFileRoutes(ctx) {
53
54
  .json({ error: true, message: err?.message ?? 'Upload failed' });
54
55
  }
55
56
  });
57
+ ctx.router.get('/files/:key(*)', async (req, res) => {
58
+ try {
59
+ let key = String(req.params.key || '');
60
+ if (!key) {
61
+ return res.status(400).json({ error: true, message: 'Key is required' });
62
+ }
63
+ // Ensure no leading slash for storage providers
64
+ key = key.replace(/^\/+/, '');
65
+ Logger_1.default.info('FileProxy', `Fetching file from storage: ${key}`);
66
+ const { body, contentType, size } = await ctx.fileStorage.download(key);
67
+ if (contentType)
68
+ res.setHeader('Content-Type', contentType);
69
+ if (size)
70
+ res.setHeader('Content-Length', size);
71
+ // Cache for 1 year if it's an image
72
+ if (contentType?.startsWith('image/')) {
73
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
74
+ }
75
+ else {
76
+ res.setHeader('Cache-Control', 'public, max-age=86400');
77
+ }
78
+ if (body && typeof body.pipe === 'function') {
79
+ body.pipe(res);
80
+ }
81
+ else if (body && typeof body.transformToByteArray === 'function') {
82
+ const data = await body.transformToByteArray();
83
+ res.send(Buffer.from(data));
84
+ }
85
+ else {
86
+ res.send(body);
87
+ }
88
+ }
89
+ catch (err) {
90
+ Logger_1.default.warn('FileProxy', `Failed to fetch file ${req.params.key}: ${err.message}`);
91
+ if (err.name === 'NoSuchKey' || err.code === 'NoSuchKey' || err.name === 'NotFound' || err.code === 'NotFound') {
92
+ return res.status(404).json({ error: true, message: 'File not found in storage' });
93
+ }
94
+ return res.status(500).json({ error: true, message: err?.message ?? 'Download failed' });
95
+ }
96
+ });
56
97
  ctx.router.post('/files', fileAuthMiddleware, upload.any(), async (req, res) => {
57
98
  try {
58
99
  const files = req.files ?? [];
@@ -55,6 +55,8 @@ export type ParsedQuery = {
55
55
  skip: number;
56
56
  projection?: Record<string, 1>;
57
57
  sort: SortSpec;
58
+ sample?: number;
59
+ _populate?: Record<string, Record<string, 1>>;
58
60
  };
59
61
  export declare function parseQuery(req: Request, allowedKeys?: Set<string>): ParsedQuery;
60
62
  export declare function extractIds(val: unknown): string[];
@@ -287,7 +287,23 @@ function parseQuery(req, allowedKeys) {
287
287
  // NOTE: do NOT force a fallback like '-createdAt' here;
288
288
  // parseSort will apply { createdAt: -1 } only if no sort is provided.
289
289
  const sort = parseSort(req.query.sort, req.query.sortType, allowedKeys);
290
- return { limit, page, skip, projection, sort };
290
+ const sampleRaw = req.query.sample;
291
+ const sample = sampleRaw ? Math.max(Number.parseInt(String(sampleRaw), 10) || 0, 0) : undefined;
292
+ const populateMap = {};
293
+ const populateRaw = String(req.query.populate ?? '').trim();
294
+ if (populateRaw) {
295
+ const parts = populateRaw.split(';');
296
+ for (const part of parts) {
297
+ const [field, subFieldsRaw] = part.split(':');
298
+ if (field && subFieldsRaw) {
299
+ const subFields = subFieldsRaw.split(',').map(s => s.trim()).filter(Boolean);
300
+ if (subFields.length > 0) {
301
+ populateMap[field] = Object.fromEntries(subFields.map(f => [f, 1]));
302
+ }
303
+ }
304
+ }
305
+ }
306
+ return { limit, page, skip, projection, sort, sample, _populate: populateMap };
291
307
  }
292
308
  function extractIds(val) {
293
309
  if (val == null)
@@ -15,6 +15,7 @@ export interface DatabaseAdapter {
15
15
  * Returns a map ofparentId -> firstRelatedId
16
16
  */
17
17
  findFirstRelatedIds(collection: string, field: string, ids: string[]): Promise<Map<string, string>>;
18
+ countBy(collection: string, field: string, query: any, schema: Schema): Promise<Record<string, number>>;
18
19
  syncIndexes?(modelName: string, desired: FieldIndexSpec): Promise<void>;
19
20
  cleanupUnusedFields?(schemas: Record<string, Schema>): Promise<Record<string, string[]>>;
20
21
  isExclusiveProjection(projection: Record<string, 0 | 1> | undefined, schema: Schema | undefined): boolean;
@@ -10,6 +10,7 @@ export declare class NMAdapter implements DatabaseAdapter {
10
10
  private repositories;
11
11
  private entitySchemas;
12
12
  private registeredSchemas;
13
+ private imageProxyOptions;
13
14
  constructor(options: any);
14
15
  connect(): Promise<void>;
15
16
  disconnect(): Promise<void>;
@@ -22,12 +23,16 @@ export declare class NMAdapter implements DatabaseAdapter {
22
23
  private coercePayloadForStorage;
23
24
  read(collection: string, query: any, limit?: number, skip?: number, schemaDefinition?: NextMinSchema, includePrivateFields?: boolean, options?: ReadOptions): Promise<any[]>;
24
25
  private prepareResult;
26
+ private transformUrls;
25
27
  private idToString;
26
28
  private convertToObjectId;
29
+ private discoverInverseRelation;
30
+ private hydrateDynamicAggregates;
27
31
  private hydrateRelations;
28
32
  update(collection: string, targetId: any, data: any, schemaDefinition?: NextMinSchema): Promise<any>;
29
33
  delete(collection: string, targetId: any, schemaDefinition?: NextMinSchema): Promise<any>;
30
34
  count(collection: string, query: any, schemaDefinition?: NextMinSchema): Promise<number>;
35
+ countBy(collection: string, field: string, query: any, schema?: NextMinSchema): Promise<Record<string, number>>;
31
36
  findFirstRelatedIds(collection: string, field: string, ids: string[]): Promise<Map<string, string>>;
32
37
  /**
33
38
  * Centralized Nested Search Resolver
@@ -19,6 +19,38 @@ class NMAdapter {
19
19
  this.repositories = new Map();
20
20
  this.entitySchemas = new Map();
21
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
+ }
22
54
  this.dataSource = new typeorm_1.DataSource({
23
55
  ...options,
24
56
  entities: [], // Will be populated dynamically
@@ -130,11 +162,13 @@ class NMAdapter {
130
162
  },
131
163
  createdAt: {
132
164
  type: 'datetime',
165
+ createDate: true,
133
166
  default: () => 'CURRENT_TIMESTAMP',
134
167
  nullable: true,
135
168
  },
136
169
  updatedAt: {
137
170
  type: 'datetime',
171
+ updateDate: true,
138
172
  default: () => 'CURRENT_TIMESTAMP',
139
173
  nullable: true,
140
174
  },
@@ -159,6 +193,9 @@ class NMAdapter {
159
193
  continue; // Base model protects this field; skip column in child
160
194
  }
161
195
  }
196
+ const head = Array.isArray(attr) ? attr[0] : attr;
197
+ if (head?.type === 'virtual')
198
+ continue;
162
199
  const typeORMType = this.mapType(attr);
163
200
  const isUnique = attr.unique === true;
164
201
  const isSparse = attr.sparse === true;
@@ -219,6 +256,12 @@ class NMAdapter {
219
256
  async create(collection, data, schemaDefinition) {
220
257
  const repo = this.getRepository(collection);
221
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;
222
265
  const created = await repo.save(coerced);
223
266
  return this.prepareResult(created);
224
267
  }
@@ -260,7 +303,16 @@ class NMAdapter {
260
303
  const hasRootProj = this.isExclusiveProjection(options.projection, schemaDefinition);
261
304
  if (hasRootProj) {
262
305
  const dbProjection = {};
306
+ // Always include ID for hydration purposes
307
+ if (isMongo)
308
+ dbProjection._id = true;
309
+ else
310
+ dbProjection.id = true;
263
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
+ }
264
316
  if (key.includes('.') && schemaDefinition) {
265
317
  const root = key.split('.')[0];
266
318
  const attr = schemaDefinition.attributes[root];
@@ -288,7 +340,15 @@ class NMAdapter {
288
340
  }
289
341
  }
290
342
  let results;
291
- if (isMongo && findOptions.projection) {
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) {
292
352
  const mongoRepo = repo.manager.getMongoRepository(repo.metadata.name);
293
353
  const cursor = mongoRepo.createCursor(where);
294
354
  cursor.project(findOptions.projection);
@@ -301,11 +361,32 @@ class NMAdapter {
301
361
  results = await cursor.toArray();
302
362
  }
303
363
  else {
304
- results = await repo.find(findOptions);
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
+ }
305
386
  }
306
387
  const mappedResults = results.map(r => this.prepareResult(r));
307
388
  if (schemaDefinition) {
308
- await this.hydrateRelations(mappedResults, schemaDefinition, options?.depth ?? 0, options?.projection);
389
+ await this.hydrateRelations(mappedResults, schemaDefinition, options?.depth ?? 0, options);
309
390
  }
310
391
  return mappedResults;
311
392
  }
@@ -324,8 +405,40 @@ class NMAdapter {
324
405
  if (item.id != null)
325
406
  item.id = String(item.id);
326
407
  }
408
+ // Apply image URL proxying/transformation if configured
409
+ if (this.imageProxyOptions.originalBase && this.imageProxyOptions.proxyBase) {
410
+ this.transformUrls(item);
411
+ }
327
412
  return item;
328
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
+ }
329
442
  idToString(val) {
330
443
  if (val == null)
331
444
  return '';
@@ -358,8 +471,15 @@ class NMAdapter {
358
471
  return this.convertToObjectId(extractedId);
359
472
  }
360
473
  try {
361
- if (typeof val === 'string' && val.length === 24 && /^[0-9a-fA-F]{24}$/.test(val)) {
362
- return new mongodb_1.ObjectId(val);
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;
363
483
  }
364
484
  return val;
365
485
  }
@@ -367,9 +487,87 @@ class NMAdapter {
367
487
  return val;
368
488
  }
369
489
  }
370
- async hydrateRelations(results, schema, depth = 0, projection) {
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) {
371
564
  if (!results.length || depth > 2)
372
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
+ }
373
571
  // 1) Handle inheritance: if schema extends another, fetch base records
374
572
  if (schema.extends) {
375
573
  const baseLc = schema.extends.toLowerCase();
@@ -436,6 +634,29 @@ class NMAdapter {
436
634
  }
437
635
  // 2) Handle standard ObjectId hydration & 3) json-group in parallel
438
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
+ }
439
660
  // --- Standard Relations ---
440
661
  const relFields = Object.entries(schema.attributes).filter(([key, attr]) => {
441
662
  if (key === 'baseId')
@@ -472,13 +693,16 @@ class NMAdapter {
472
693
  }
473
694
  if (idsToFetch.size === 0)
474
695
  return;
475
- let subProjection = undefined;
476
- if (projection) {
696
+ let subProjection = options?._populate?.[key];
697
+ if (subProjection) {
698
+ subProjection = { ...subProjection, id: 1 };
699
+ }
700
+ if (!subProjection && projection) {
477
701
  const prefix = `${key}.`;
478
702
  for (const [pk, pv] of Object.entries(projection)) {
479
703
  if (pk.startsWith(prefix)) {
480
704
  if (!subProjection)
481
- subProjection = {};
705
+ subProjection = { id: 1 };
482
706
  subProjection[pk.slice(prefix.length)] = pv;
483
707
  }
484
708
  }
@@ -554,6 +778,8 @@ class NMAdapter {
554
778
  const coerced = this.coercePayloadForStorage(data, schemaDefinition);
555
779
  // Strip ID fields from data to prevent primary key conflicts
556
780
  const { id: _unusedId, _id: _unusedRawId, ...updateData } = coerced;
781
+ const now = new Date();
782
+ updateData.updatedAt = now;
557
783
  if (isMongo) {
558
784
  // Using explicit getMongoRepository and updateOne prevents accidental bulk updates
559
785
  // that targetBy filter {} could cause in standard TypeORM .update()
@@ -572,6 +798,8 @@ class NMAdapter {
572
798
  setObj[k] = updateData[k] !== undefined ? updateData[k] : v;
573
799
  }
574
800
  }
801
+ // Always update the updatedAt field
802
+ setObj.updatedAt = now;
575
803
  const updatePayload = {};
576
804
  if (Object.keys(setObj).length > 0)
577
805
  updatePayload.$set = setObj;
@@ -622,6 +850,42 @@ class NMAdapter {
622
850
  }
623
851
  return total;
624
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
+ }
625
889
  async findFirstRelatedIds(collection, field, ids) {
626
890
  const repo = this.getRepository(collection);
627
891
  const map = new Map();
@@ -28,6 +28,11 @@ export interface UploadResult {
28
28
  export interface FileStorageAdapter {
29
29
  readonly name: FileProvider;
30
30
  upload(input: UploadPayload): Promise<UploadResult>;
31
+ download(key: string): Promise<{
32
+ body: any;
33
+ contentType?: string;
34
+ size?: number;
35
+ }>;
31
36
  delete(key: string): Promise<{
32
37
  deleted: boolean;
33
38
  }>;
@@ -9,6 +9,11 @@ export declare class LocalFileStorageAdapter implements FileStorageAdapter {
9
9
  private publicUrlPrefix;
10
10
  constructor(options: LocalFileStorageOptions);
11
11
  upload(input: UploadPayload): Promise<UploadResult>;
12
+ download(key: string): Promise<{
13
+ body: any;
14
+ contentType?: string;
15
+ size?: number;
16
+ }>;
12
17
  delete(key: string): Promise<{
13
18
  deleted: boolean;
14
19
  }>;
@@ -43,6 +43,20 @@ class LocalFileStorageAdapter {
43
43
  contentType: input.contentType,
44
44
  };
45
45
  }
46
+ async download(key) {
47
+ const filePath = path_1.default.join(this.uploadDir, key);
48
+ if (!fs_1.default.existsSync(filePath)) {
49
+ throw new Error('File not found');
50
+ }
51
+ const stats = fs_1.default.statSync(filePath);
52
+ const body = fs_1.default.createReadStream(filePath);
53
+ return {
54
+ body,
55
+ size: stats.size,
56
+ // Note: Local storage doesn't store contentType natively in metadata here,
57
+ // but we could infer it if needed.
58
+ };
59
+ }
46
60
  async delete(key) {
47
61
  const filePath = path_1.default.join(this.uploadDir, key);
48
62
  if (fs_1.default.existsSync(filePath)) {
@@ -22,6 +22,11 @@ export declare class S3FileStorageAdapter implements FileStorageAdapter {
22
22
  private readonly publicBaseUrl?;
23
23
  constructor(opts: S3AdapterOptions);
24
24
  upload(input: UploadPayload): Promise<UploadResult>;
25
+ download(key: string): Promise<{
26
+ body: any;
27
+ contentType?: string;
28
+ size?: number;
29
+ }>;
25
30
  delete(key: string): Promise<{
26
31
  deleted: boolean;
27
32
  }>;
@@ -55,6 +55,20 @@ class S3FileStorageAdapter {
55
55
  metadata: input.metadata,
56
56
  };
57
57
  }
58
+ async download(key) {
59
+ try {
60
+ const res = await this.s3.send(new client_s3_1.GetObjectCommand({ Bucket: this.bucket, Key: key }));
61
+ return {
62
+ body: res.Body,
63
+ contentType: res.ContentType,
64
+ size: res.ContentLength,
65
+ };
66
+ }
67
+ catch (err) {
68
+ // Re-throw so the caller can log it with more context
69
+ throw err;
70
+ }
71
+ }
58
72
  async delete(key) {
59
73
  await this.s3.send(new client_s3_1.DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
60
74
  return { deleted: true };
@@ -5,6 +5,8 @@ export type ReadOptions = {
5
5
  sort?: SortSpec;
6
6
  projection?: Record<string, 0 | 1>;
7
7
  depth?: number;
8
+ sample?: number;
9
+ _populate?: Record<string, Record<string, 1>>;
8
10
  };
9
11
  export interface Attributes {
10
12
  type: string;
@@ -21,6 +23,9 @@ export interface Attributes {
21
23
  rich?: boolean;
22
24
  searchBy?: string;
23
25
  show?: string;
26
+ op?: 'count' | 'sum' | 'avg' | 'min' | 'max';
27
+ foreignField?: string;
28
+ localField?: string;
24
29
  }
25
30
  export interface CustomAction {
26
31
  label: string;
@@ -47,15 +52,23 @@ export interface Schema {
47
52
  extends?: string;
48
53
  actions?: CustomAction[];
49
54
  access?: any;
55
+ aggregates?: AggregateSpec[];
56
+ }
57
+ export interface AggregateSpec {
58
+ sourceModel: string;
59
+ foreignField: string;
60
+ op: 'count' | 'avg' | 'sum' | 'min' | 'max';
61
+ field?: string;
62
+ targetField: string;
50
63
  }
51
64
  export declare class BaseModel {
52
- private schema;
65
+ schema: Schema;
53
66
  private adapter;
54
67
  private collectionName;
55
68
  constructor(schema: Schema, adapter: DatabaseAdapter);
56
69
  create(data: any, includePrivateFields?: boolean): Promise<any>;
57
70
  read(query: any, limit?: number, skip?: number, includePrivateFields?: boolean, options?: ReadOptions): Promise<any[]>;
58
- update(id: string, data: any, includePrivateFields?: boolean): Promise<any>;
71
+ update(id: string, data: any, includePrivateFields?: boolean, oldDoc?: any): Promise<any>;
59
72
  delete(id: string, includePrivateFields?: boolean): Promise<any>;
60
73
  count(query: any, includePrivateFields?: boolean): Promise<number>;
61
74
  findFirstRelatedIds(field: string, ids: string[]): Promise<Map<string, string>>;
@@ -31,13 +31,13 @@ class BaseModel {
31
31
  Events_1.events.emitEvent((0, Events_1.getModelEvent)(this.schema.modelName, 'read', 'after'), { ...payload, result });
32
32
  return result;
33
33
  }
34
- async update(id, data, includePrivateFields) {
35
- const payload = { modelName: this.schema.modelName, id, data };
34
+ async update(id, data, includePrivateFields, oldDoc) {
35
+ const payload = { modelName: this.schema.modelName, id, data, oldDoc };
36
36
  Events_1.events.emitEvent(Events_1.Events.BEFORE_UPDATE, payload);
37
37
  Events_1.events.emitEvent((0, Events_1.getModelEvent)(this.schema.modelName, 'update', 'before'), payload);
38
38
  const result = await this.adapter.update(this.collectionName, id, data, this.schema, includePrivateFields);
39
- Events_1.events.emitEvent(Events_1.Events.AFTER_UPDATE, { ...payload, result });
40
- Events_1.events.emitEvent((0, Events_1.getModelEvent)(this.schema.modelName, 'update', 'after'), { ...payload, result });
39
+ Events_1.events.emitEvent(Events_1.Events.AFTER_UPDATE, { ...payload, result, oldDoc });
40
+ Events_1.events.emitEvent((0, Events_1.getModelEvent)(this.schema.modelName, 'update', 'after'), { ...payload, result, oldDoc });
41
41
  return result;
42
42
  }
43
43
  async delete(id, includePrivateFields) {
@@ -0,0 +1,8 @@
1
+ import type { RouterCtx } from '../api/router/ctx';
2
+ export declare class AggregateService {
3
+ private ctx;
4
+ constructor(ctx: RouterCtx);
5
+ private init;
6
+ private handleUpdate;
7
+ private recalculate;
8
+ }
@@ -0,0 +1,87 @@
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.AggregateService = void 0;
7
+ const Events_1 = require("../utils/Events");
8
+ const Logger_1 = __importDefault(require("../utils/Logger"));
9
+ class AggregateService {
10
+ constructor(ctx) {
11
+ this.ctx = ctx;
12
+ this.init();
13
+ }
14
+ init() {
15
+ Events_1.events.on(Events_1.Events.AFTER_CREATE, (payload) => this.handleUpdate(payload.modelName, payload.result, 'create'));
16
+ Events_1.events.on(Events_1.Events.AFTER_UPDATE, (payload) => this.handleUpdate(payload.modelName, payload.result, 'update'));
17
+ Events_1.events.on(Events_1.Events.AFTER_DELETE, (payload) => this.handleUpdate(payload.modelName, payload.result, 'delete'));
18
+ Logger_1.default.info('AggregateService', 'Initialized and listening for CRUD events');
19
+ }
20
+ async handleUpdate(sourceModelName, doc, action) {
21
+ if (!doc)
22
+ return;
23
+ // Find SCHEMAS that have aggregates dependent on sourceModelName
24
+ const allSchemas = Object.values(this.ctx.liveSchemas);
25
+ for (const targetSchema of allSchemas) {
26
+ if (!targetSchema.aggregates)
27
+ continue;
28
+ const ags = targetSchema.aggregates.filter((a) => a.sourceModel.toLowerCase() === sourceModelName.toLowerCase());
29
+ if (ags.length === 0)
30
+ continue;
31
+ for (const ag of ags) {
32
+ const parentId = doc[ag.foreignField];
33
+ if (!parentId)
34
+ continue;
35
+ // Recalculate and update target document (Doctor)
36
+ await this.recalculate(targetSchema.modelName, parentId, ag);
37
+ }
38
+ }
39
+ }
40
+ async recalculate(targetModelName, targetId, ag) {
41
+ try {
42
+ const sourceModel = this.ctx.getModel(ag.sourceModel.toLowerCase());
43
+ const targetModel = this.ctx.getModel(targetModelName.toLowerCase());
44
+ const filter = { [ag.foreignField]: targetId };
45
+ let result = 0;
46
+ if (ag.op === 'count') {
47
+ result = await sourceModel.count(filter, true);
48
+ }
49
+ else if (ag.op === 'avg' || ag.op === 'sum') {
50
+ const adapter = this.ctx.dbAdapter;
51
+ if (adapter.dataSource.options.type === 'mongodb') {
52
+ const repo = adapter.dataSource.getMongoRepository(ag.sourceModel);
53
+ const aggregation = [
54
+ { $match: adapter.transformQuery(filter, sourceModel.schema) },
55
+ { $group: { _id: null, val: { [`$${ag.op}`]: `$${ag.field}` } } },
56
+ ];
57
+ const res = await repo.aggregate(aggregation).toArray();
58
+ result = res[0]?.val || 0;
59
+ }
60
+ else {
61
+ // SQL
62
+ const repo = adapter.repositories.get(ag.sourceModel.toLowerCase());
63
+ const qb = repo.createQueryBuilder('entity');
64
+ qb.select(`${ag.op === 'avg' ? 'AVG' : 'SUM'}(entity.${ag.field})`, 'val');
65
+ qb.where(adapter.transformQuery(filter, sourceModel.schema));
66
+ const res = await qb.getRawOne();
67
+ result = parseFloat(res?.val || res?.VAL || 0);
68
+ }
69
+ }
70
+ // Update target document (e.g. Doctor)
71
+ const updateData = {};
72
+ const parts = ag.targetField.split('.');
73
+ let curr = updateData;
74
+ for (let i = 0; i < parts.length - 1; i++) {
75
+ curr[parts[i]] = {};
76
+ curr = curr[parts[i]];
77
+ }
78
+ curr[parts[parts.length - 1]] = result;
79
+ await targetModel.update(targetId, updateData, true);
80
+ Logger_1.default.debug('AggregateService', `Updated ${targetModelName}:${targetId} aggregate ${ag.targetField} to ${result}`);
81
+ }
82
+ catch (err) {
83
+ Logger_1.default.error('AggregateService', `Failed to recalculate aggregate for ${targetModelName}:${targetId}`, err);
84
+ }
85
+ }
86
+ }
87
+ exports.AggregateService = AggregateService;
@@ -21,7 +21,7 @@ class NextMinEvents extends events_1.EventEmitter {
21
21
  * Typed emit to provide better DX (Developer Experience)
22
22
  */
23
23
  emitEvent(event, payload) {
24
- Logger_1.default.info('NextMinEvents', `Emitting event: ${event}`);
24
+ Logger_1.default.debug('NextMinEvents', `Emitting event: ${event}`);
25
25
  this.emit(event, payload);
26
26
  }
27
27
  }
@@ -1,8 +1,9 @@
1
1
  export declare class Logger {
2
- private isDevelopment;
2
+ private get isDevelopment();
3
3
  constructor();
4
4
  private getCallerInfo;
5
5
  private formatMessage;
6
+ debug(label: string, ...messages: any[]): void;
6
7
  log(label: string, ...messages: any[]): void;
7
8
  info(label: string, ...messages: any[]): void;
8
9
  warn(label: string, ...messages: any[]): void;
@@ -8,9 +8,11 @@ const kleur_1 = __importDefault(require("kleur"));
8
8
  const util_1 = require("util");
9
9
  const path_1 = __importDefault(require("path"));
10
10
  class Logger {
11
- constructor() {
12
- this.isDevelopment = process.env.APP_MODE !== 'production' && process.env.NODE_ENV !== 'production';
11
+ get isDevelopment() {
12
+ return (process.env.APP_MODE?.toLowerCase() !== 'production' &&
13
+ process.env.NODE_ENV?.toLowerCase() !== 'production');
13
14
  }
15
+ constructor() { }
14
16
  getCallerInfo() {
15
17
  const originalPrepareStackTrace = Error.prepareStackTrace;
16
18
  Error.prepareStackTrace = (err, stack) => stack;
@@ -45,12 +47,21 @@ class Logger {
45
47
  case 'ERROR':
46
48
  formattedLabel = kleur_1.default.red(`[ERROR] ${label} (${callerInfo}):`);
47
49
  break;
50
+ case 'DEBUG':
51
+ formattedLabel = kleur_1.default.magenta(`[DEBUG] ${label} (${callerInfo}):`);
52
+ break;
48
53
  default:
49
54
  formattedLabel = `[${level}] ${label} (${callerInfo}):`;
50
55
  }
51
56
  const formattedMessage = (0, util_1.format)(...messages);
52
57
  return `${formattedLabel} ${formattedMessage}`;
53
58
  }
59
+ debug(label, ...messages) {
60
+ if (this.isDevelopment) {
61
+ const formatted = this.formatMessage('DEBUG', label, messages);
62
+ console.debug(formatted);
63
+ }
64
+ }
54
65
  log(label, ...messages) {
55
66
  if (this.isDevelopment) {
56
67
  const formatted = this.formatMessage('LOG', label, messages);
@@ -70,10 +81,8 @@ class Logger {
70
81
  }
71
82
  }
72
83
  error(label, ...messages) {
73
- if (this.isDevelopment) {
74
- const formatted = this.formatMessage('ERROR', label, messages);
75
- console.error(formatted);
76
- }
84
+ const formatted = this.formatMessage('ERROR', label, messages);
85
+ console.error(formatted);
77
86
  }
78
87
  }
79
88
  exports.Logger = Logger;
@@ -17,7 +17,7 @@ export declare class SchemaLoader {
17
17
  schemas: {
18
18
  [key: string]: Schema;
19
19
  };
20
- private isDevelopment;
20
+ private get isDevelopment();
21
21
  private nonOverridableSchemas;
22
22
  private userSchemasDir;
23
23
  private packageSchemasDir;
@@ -58,6 +58,10 @@ function validateSchema(obj) {
58
58
  return missing;
59
59
  }
60
60
  class SchemaLoader {
61
+ get isDevelopment() {
62
+ return (process.env.APP_MODE?.toLowerCase() !== 'production' &&
63
+ process.env.NODE_ENV?.toLowerCase() !== 'production');
64
+ }
61
65
  // Singleton instance
62
66
  static getInstance() {
63
67
  if (!SchemaLoader._instance) {
@@ -68,10 +72,8 @@ class SchemaLoader {
68
72
  constructor() {
69
73
  this.emitter = new events_1.EventEmitter();
70
74
  this.schemas = {};
71
- this.isDevelopment = false;
72
75
  this.nonOverridableSchemas = new Set(['User', 'Role']);
73
76
  this.watcher = null; // <-- TS-safe
74
- this.isDevelopment = process.env.APP_MODE !== 'production';
75
77
  const baseDir = process.cwd();
76
78
  const basePackageDir = path_1.default.resolve(__dirname, '..');
77
79
  this.packageSchemasDir = path_1.default.join(basePackageDir, 'schemas');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@airoom/nextmin-node",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",