@airoom/nextmin-node 2.0.1 → 2.0.3

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 (45) hide show
  1. package/README.md +151 -0
  2. package/dist/api/apiRouter.d.ts +28 -1
  3. package/dist/api/apiRouter.js +138 -8
  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 +79 -20
  7. package/dist/api/router/setupAuthRoutes.js +475 -38
  8. package/dist/api/router/setupChatWidgetRoutes.d.ts +3 -0
  9. package/dist/api/router/setupChatWidgetRoutes.js +207 -0
  10. package/dist/api/router/setupFileRoutes.js +258 -12
  11. package/dist/api/router/utils.d.ts +4 -1
  12. package/dist/api/router/utils.js +25 -7
  13. package/dist/cli.d.ts +1 -0
  14. package/dist/cli.js +145 -64
  15. package/dist/database/DatabaseAdapter.d.ts +2 -0
  16. package/dist/database/NMAdapter.d.ts +11 -0
  17. package/dist/database/NMAdapter.js +688 -73
  18. package/dist/database/QueryEngine.js +7 -4
  19. package/dist/files/FileStorageAdapter.d.ts +6 -0
  20. package/dist/files/LocalFileStorageAdapter.d.ts +6 -0
  21. package/dist/files/LocalFileStorageAdapter.js +39 -3
  22. package/dist/files/S3FileStorageAdapter.d.ts +8 -1
  23. package/dist/files/S3FileStorageAdapter.js +86 -11
  24. package/dist/files/filename.js +6 -4
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +3 -1
  27. package/dist/models/BaseModel.d.ts +19 -2
  28. package/dist/models/BaseModel.js +4 -4
  29. package/dist/policy/authorize.d.ts +1 -1
  30. package/dist/policy/authorize.js +64 -13
  31. package/dist/schemas/Users.json +20 -10
  32. package/dist/services/AggregateService.d.ts +8 -0
  33. package/dist/services/AggregateService.js +87 -0
  34. package/dist/services/IndexingService.d.ts +24 -0
  35. package/dist/services/IndexingService.js +555 -0
  36. package/dist/services/LocalAIService.d.ts +34 -0
  37. package/dist/services/LocalAIService.js +471 -0
  38. package/dist/services/OpenAIService.d.ts +16 -0
  39. package/dist/services/OpenAIService.js +586 -0
  40. package/dist/utils/Events.js +1 -1
  41. package/dist/utils/Logger.d.ts +2 -1
  42. package/dist/utils/Logger.js +15 -6
  43. package/dist/utils/SchemaLoader.d.ts +2 -2
  44. package/dist/utils/SchemaLoader.js +44 -6
  45. package/package.json +12 -4
@@ -36,6 +36,35 @@ async function injectActionRelatedData(ctx, schema, rows) {
36
36
  }
37
37
  }
38
38
  const utils_1 = require("./utils");
39
+ /**
40
+ * For User-extending schemas: strip inherited fields from a response doc
41
+ * when no base user is linked (baseId is null/empty).
42
+ * This keeps API responses clean — no null Username, First Name, etc.
43
+ */
44
+ function stripInheritedIfNoBase(doc, schema) {
45
+ if (!doc || !schema?.extends)
46
+ return doc;
47
+ const isUserExt = String(schema.extends).toLowerCase() === 'users';
48
+ if (!isUserExt)
49
+ return doc;
50
+ const hasBase = !!(doc.baseId || doc.exId);
51
+ if (hasBase)
52
+ return doc;
53
+ const attrs = schema.attributes || {};
54
+ const result = { ...doc };
55
+ for (const [key, attr] of Object.entries(attrs)) {
56
+ const head = Array.isArray(attr) ? attr[0] : attr;
57
+ if (head?.inherited) {
58
+ delete result[key];
59
+ }
60
+ }
61
+ return result;
62
+ }
63
+ function stripInheritedIfNoBaseMany(docs, schema) {
64
+ if (!schema?.extends)
65
+ return docs;
66
+ return docs.map(d => stripInheritedIfNoBase(d, schema));
67
+ }
39
68
  function mountCrudRoutes(ctx, modelNameLC) {
40
69
  const { router } = ctx;
41
70
  const basePath = `/${modelNameLC}`;
@@ -93,7 +122,14 @@ function mountCrudRoutes(ctx, modelNameLC) {
93
122
  return res.status(403).json({ error: true, message: 'forbidden' });
94
123
  }
95
124
  let payload = (0, authorize_1.mergeCreateDefaults)(req.body, cdec.createDefaults);
96
- (0, authorize_1.enforceRestrictions)(payload, cdec.restrictions, pctx);
125
+ // Apply schema attribute defaults if missing in the request body
126
+ // For extended schemas, skip defaults for inherited attributes (they belong to the base record)
127
+ for (const [key, attr] of Object.entries(schema.attributes || {})) {
128
+ if (!Array.isArray(attr) && attr?.default !== undefined && payload[key] === undefined && !attr?.inherited) {
129
+ payload[key] = attr.default;
130
+ }
131
+ }
132
+ await (0, authorize_1.enforceRestrictions)(payload, cdec.restrictions, pctx, ctx);
97
133
  if (!cdec.exposePrivate)
98
134
  payload = (0, authorize_1.stripWriteDeny)(payload, cdec.writeDeny);
99
135
  payload = (0, fieldCodecs_1.coerceForStorage)(schema, payload);
@@ -126,6 +162,9 @@ function mountCrudRoutes(ctx, modelNameLC) {
126
162
  const basePayload = {};
127
163
  const childPayload = {};
128
164
  for (const [k, v] of Object.entries(payload)) {
165
+ if (k === 'id' || k === '_id' || k === 'exId' || k === 'createdAt' || k === 'updatedAt') {
166
+ continue;
167
+ }
129
168
  if (k === 'baseId') {
130
169
  childPayload[k] = v;
131
170
  continue;
@@ -164,7 +203,13 @@ function mountCrudRoutes(ctx, modelNameLC) {
164
203
  // Only attempt creation if at least one base field is provided or if it's not a UserExt?
165
204
  // Actually, for UserExt, if baseId is missing and they didn't fill user fields (which are hidden),
166
205
  // we should skip base creation and just create the child.
167
- if (baseMissing.length === 0 && Object.keys(basePayload).length > 0) {
206
+ // For User extensions: only create a base user if meaningful user fields
207
+ // are explicitly provided (not just auto-injected defaults like status, type, role)
208
+ const AUTO_DEFAULT_KEYS = new Set(['status', 'type', 'role', 'createdBy']);
209
+ const hasMeaningfulBaseData = isUserExt
210
+ ? Object.keys(basePayload).some(k => !AUTO_DEFAULT_KEYS.has(k))
211
+ : Object.keys(basePayload).length > 0;
212
+ if (baseMissing.length === 0 && hasMeaningfulBaseData) {
168
213
  if (baseLC === 'users' && basePayload.password && ctx.jwtSecret) {
169
214
  const salt = await bcrypt_1.default.genSalt(10);
170
215
  basePayload.password = await bcrypt_1.default.hash(String(basePayload.password) + ctx.jwtSecret, salt);
@@ -231,7 +276,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
231
276
  return res.status(201).json({
232
277
  success: true,
233
278
  message: `${schema.modelName} has been created successfully.`,
234
- data: masked,
279
+ data: stripInheritedIfNoBase(masked, schema),
235
280
  });
236
281
  }
237
282
  // non-extended
@@ -275,7 +320,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
275
320
  }
276
321
  const model = ctx.getModel(modelNameLC);
277
322
  const baseKeys = new Set(Object.keys(schema.attributes || {}));
278
- const { limit, page, skip, sort, projection } = (0, utils_1.parseQuery)(req, baseKeys);
323
+ const { limit, page, skip, sort, projection, _populate } = (0, utils_1.parseQuery)(req, baseKeys);
279
324
  const q = String(req.query.q ?? '').trim();
280
325
  const searchKey = String(req.query.searchKey ?? '').trim();
281
326
  const searchKeysCSV = String(req.query.searchKeys ?? '').trim();
@@ -296,6 +341,14 @@ function mountCrudRoutes(ctx, modelNameLC) {
296
341
  return res.status(403).json({ error: true, message: 'forbidden' });
297
342
  }
298
343
  const finalFilter = (0, authorize_1.andFilter)(filter, rdec.queryFilter || {});
344
+ const countBy = String(req.query.countBy ?? '').trim();
345
+ if (countBy && schema.attributes[countBy]) {
346
+ const counts = await ctx.dbAdapter.countBy(schema.modelName, countBy, finalFilter, schema);
347
+ return res.status(200).json({
348
+ success: true,
349
+ counts,
350
+ });
351
+ }
299
352
  const isUsersModel = modelNameLC === 'user' ||
300
353
  modelNameLC === 'users' ||
301
354
  (schema?.modelName ?? '').toLowerCase() === 'users';
@@ -322,13 +375,14 @@ function mountCrudRoutes(ctx, modelNameLC) {
322
375
  let dataRows = [];
323
376
  if (requiresBaseProcessing) {
324
377
  // If we need to filter/sort by base fields, read ALL child rows (already hydrated by adapter)
325
- dataRows = await model.read(childFilter, 0, 0, needPrivateForHydrate, { projection });
378
+ dataRows = await model.read(childFilter, 0, 0, needPrivateForHydrate, { projection, _populate });
326
379
  }
327
380
  else {
328
381
  // Normal paginated read (already hydrated by adapter)
329
382
  dataRows = await model.read(childFilter, limit, skip, needPrivateForHydrate, {
330
383
  sort: Object.keys(childSort).length ? childSort : undefined,
331
384
  projection,
385
+ _populate,
332
386
  });
333
387
  }
334
388
  let merged = dataRows;
@@ -369,9 +423,6 @@ function mountCrudRoutes(ctx, modelNameLC) {
369
423
  }
370
424
  // **dedupe**: one row per base entity
371
425
  merged = (0, utils_1.dedupeBy)(merged, (r) => String(r.exId || r.id || r._id || ''));
372
- if (currentUserId && isUsersModel) {
373
- merged = merged.filter((r) => String(r?.id) !== String(currentUserId));
374
- }
375
426
  // pagination + totals
376
427
  let totalRows;
377
428
  let paged;
@@ -392,7 +443,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
392
443
  return res.status(200).json({
393
444
  success: true,
394
445
  message: `Data fetched for ${schema.modelName}`,
395
- data,
446
+ data: stripInheritedIfNoBaseMany(data, schema),
396
447
  pagination: { totalRows, page, limit },
397
448
  sort: combinedSort,
398
449
  });
@@ -402,7 +453,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
402
453
  let rawRows = [];
403
454
  const exposePrivateForRead = !!rdec.exposePrivate;
404
455
  try {
405
- rawRows = await model.read(finalFilter, limit + 1, skip, exposePrivateForRead, { sort, projection });
456
+ rawRows = await model.read(finalFilter, limit + 1, skip, exposePrivateForRead, { sort, projection, _populate });
406
457
  }
407
458
  catch {
408
459
  rawRows = await model.read(finalFilter, limit + 1, skip, exposePrivateForRead);
@@ -430,11 +481,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
430
481
  });
431
482
  }
432
483
  }
433
- let rows = currentUserId && isUsersModel
434
- ? rawRows
435
- .filter((r) => String(r?.id) !== String(currentUserId))
436
- .slice(0, limit)
437
- : rawRows.slice(0, limit);
484
+ let rows = rawRows.slice(0, limit);
438
485
  const data = rdec.exposePrivate
439
486
  ? (0, authorize_1.applyReadMaskMany)(rows, rdec.sensitiveMask)
440
487
  : (0, authorize_1.applyReadMaskMany)(rows, rdec.readMask);
@@ -516,7 +563,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
516
563
  return res.status(200).json({
517
564
  success: true,
518
565
  message: `${schema.modelName} found`,
519
- data,
566
+ data: stripInheritedIfNoBase(data, schema),
520
567
  });
521
568
  }
522
569
  catch (error) {
@@ -548,9 +595,18 @@ function mountCrudRoutes(ctx, modelNameLC) {
548
595
  return res.status(403).json({ error: true, message: 'forbidden' });
549
596
  }
550
597
  let upd = { ...req.body };
551
- (0, authorize_1.enforceRestrictions)(upd, udec.restrictions, pctx);
598
+ await (0, authorize_1.enforceRestrictions)(upd, udec.restrictions, pctx, ctx);
552
599
  if (!udec.exposePrivate)
553
600
  upd = (0, authorize_1.stripWriteDeny)(upd, udec.writeDeny);
601
+ // Enforce schema readOnly / non-editable fields on update
602
+ if (schema?.attributes) {
603
+ for (const [attrName, attrDef] of Object.entries(schema.attributes)) {
604
+ const a = Array.isArray(attrDef) ? attrDef[0] : attrDef;
605
+ if (a?.readOnly === true || a?.editable === false) {
606
+ delete upd[attrName];
607
+ }
608
+ }
609
+ }
554
610
  upd = (0, fieldCodecs_1.coerceForStorage)(schema, upd);
555
611
  if (modelNameLC === 'users' &&
556
612
  Object.prototype.hasOwnProperty.call(upd, 'password')) {
@@ -581,6 +637,9 @@ function mountCrudRoutes(ctx, modelNameLC) {
581
637
  const baseUpd = {};
582
638
  const childUpd = {};
583
639
  for (const [k, v] of Object.entries(upd)) {
640
+ if (k === 'id' || k === '_id' || k === 'exId' || k === 'createdAt' || k === 'updatedAt') {
641
+ continue;
642
+ }
584
643
  if (k === 'baseId') {
585
644
  childUpd[k] = v;
586
645
  continue;
@@ -640,7 +699,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
640
699
  }
641
700
  let updatedChild = before;
642
701
  if (Object.keys(childUpd).length) {
643
- updatedChild = await model.update(req.params.id, childUpd);
702
+ updatedChild = await model.update(req.params.id, childUpd, false, before);
644
703
  }
645
704
  if (targetBaseId && Object.keys(baseUpd).length) {
646
705
  await baseModel.update(targetBaseId, baseUpd);
@@ -672,7 +731,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
672
731
  const masked = udec.exposePrivate
673
732
  ? (0, authorize_1.applyReadMaskOne)(responseDoc, udec.sensitiveMask)
674
733
  : (0, authorize_1.applyReadMaskOne)(responseDoc, udec.readMask);
675
- return res.json(masked);
734
+ return res.json(stripInheritedIfNoBase(masked, schema));
676
735
  }
677
736
  // non-extended
678
737
  const conflicts = await ctx.checkUniqueFields(schema, upd, req.params.id);
@@ -687,7 +746,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
687
746
  })),
688
747
  });
689
748
  }
690
- const updatedRecord = await model.update(req.params.id, upd);
749
+ const updatedRecord = await model.update(req.params.id, upd, false, before);
691
750
  let responseDoc = updatedRecord;
692
751
  if (udec.exposePrivate && updatedRecord?.id) {
693
752
  const refetched = await model.read({ id: updatedRecord.id }, 1, 0, true);