@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.
- package/README.md +151 -0
- package/dist/api/apiRouter.d.ts +28 -1
- package/dist/api/apiRouter.js +138 -8
- package/dist/api/router/mountBatchRoutes.d.ts +2 -0
- package/dist/api/router/mountBatchRoutes.js +90 -0
- package/dist/api/router/mountCrudRoutes.js +79 -20
- package/dist/api/router/setupAuthRoutes.js +475 -38
- package/dist/api/router/setupChatWidgetRoutes.d.ts +3 -0
- package/dist/api/router/setupChatWidgetRoutes.js +207 -0
- package/dist/api/router/setupFileRoutes.js +258 -12
- package/dist/api/router/utils.d.ts +4 -1
- package/dist/api/router/utils.js +25 -7
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +145 -64
- package/dist/database/DatabaseAdapter.d.ts +2 -0
- package/dist/database/NMAdapter.d.ts +11 -0
- package/dist/database/NMAdapter.js +688 -73
- package/dist/database/QueryEngine.js +7 -4
- package/dist/files/FileStorageAdapter.d.ts +6 -0
- package/dist/files/LocalFileStorageAdapter.d.ts +6 -0
- package/dist/files/LocalFileStorageAdapter.js +39 -3
- package/dist/files/S3FileStorageAdapter.d.ts +8 -1
- package/dist/files/S3FileStorageAdapter.js +86 -11
- package/dist/files/filename.js +6 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/models/BaseModel.d.ts +19 -2
- package/dist/models/BaseModel.js +4 -4
- package/dist/policy/authorize.d.ts +1 -1
- package/dist/policy/authorize.js +64 -13
- package/dist/schemas/Users.json +20 -10
- package/dist/services/AggregateService.d.ts +8 -0
- package/dist/services/AggregateService.js +87 -0
- package/dist/services/IndexingService.d.ts +24 -0
- package/dist/services/IndexingService.js +555 -0
- package/dist/services/LocalAIService.d.ts +34 -0
- package/dist/services/LocalAIService.js +471 -0
- package/dist/services/OpenAIService.d.ts +16 -0
- package/dist/services/OpenAIService.js +586 -0
- package/dist/utils/Events.js +1 -1
- package/dist/utils/Logger.d.ts +2 -1
- package/dist/utils/Logger.js +15 -6
- package/dist/utils/SchemaLoader.d.ts +2 -2
- package/dist/utils/SchemaLoader.js +44 -6
- 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
|
-
|
|
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
|
-
|
|
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 =
|
|
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);
|