@airoom/nextmin-node 1.4.6 → 2.0.1
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 +48 -5
- package/dist/api/apiRouter.d.ts +2 -0
- package/dist/api/apiRouter.js +67 -21
- package/dist/api/router/mountCrudRoutes.js +207 -220
- package/dist/api/router/mountFindRoutes.js +2 -49
- package/dist/api/router/mountSearchRoutes.js +10 -52
- package/dist/api/router/mountSearchRoutes_extended.js +7 -48
- package/dist/api/router/utils.js +20 -7
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +83 -0
- package/dist/database/DatabaseAdapter.d.ts +7 -0
- package/dist/database/NMAdapter.d.ts +41 -0
- package/dist/database/NMAdapter.js +979 -0
- package/dist/database/QueryEngine.d.ts +14 -0
- package/dist/database/QueryEngine.js +215 -0
- package/dist/database/utils.d.ts +2 -0
- package/dist/database/utils.js +21 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +11 -5
- package/dist/models/BaseModel.d.ts +16 -0
- package/dist/models/BaseModel.js +32 -4
- package/dist/policy/authorize.js +95 -38
- package/dist/schemas/Users.json +66 -30
- package/dist/services/RealtimeService.d.ts +20 -0
- package/dist/services/RealtimeService.js +93 -0
- package/dist/services/SchemaService.d.ts +3 -0
- package/dist/services/SchemaService.js +6 -2
- package/dist/utils/DefaultDataInitializer.js +10 -2
- package/dist/utils/Events.d.ts +34 -0
- package/dist/utils/Events.js +55 -0
- package/dist/utils/Logger.js +12 -10
- package/dist/utils/QueryCache.d.ts +16 -0
- package/dist/utils/QueryCache.js +106 -0
- package/dist/utils/SchemaLoader.d.ts +5 -0
- package/dist/utils/SchemaLoader.js +45 -3
- package/package.json +19 -4
- package/dist/database/InMemoryAdapter.d.ts +0 -15
- package/dist/database/InMemoryAdapter.js +0 -71
- package/dist/database/MongoAdapter.d.ts +0 -52
- package/dist/database/MongoAdapter.js +0 -410
|
@@ -8,6 +8,33 @@ const bcrypt_1 = __importDefault(require("bcrypt"));
|
|
|
8
8
|
const Logger_1 = __importDefault(require("../../utils/Logger"));
|
|
9
9
|
const fieldCodecs_1 = require("../../utils/fieldCodecs");
|
|
10
10
|
const authorize_1 = require("../../policy/authorize");
|
|
11
|
+
const QueryEngine_1 = require("../../database/QueryEngine");
|
|
12
|
+
async function injectActionRelatedData(ctx, schema, rows) {
|
|
13
|
+
const actionsWithRelated = schema.actions?.filter((a) => a.relatedModel && a.relatedField);
|
|
14
|
+
if (!actionsWithRelated?.length || rows.length === 0)
|
|
15
|
+
return;
|
|
16
|
+
const ids = rows.map((r) => String(r.id));
|
|
17
|
+
for (const action of actionsWithRelated) {
|
|
18
|
+
try {
|
|
19
|
+
const model = ctx.getModel(action.relatedModel.toLowerCase());
|
|
20
|
+
if (model) {
|
|
21
|
+
// Precise grouping lookup instead of limited read()
|
|
22
|
+
const relatedMap = await model.findFirstRelatedIds(action.relatedField, ids);
|
|
23
|
+
for (const row of rows) {
|
|
24
|
+
if (!row._actionsData)
|
|
25
|
+
row._actionsData = {};
|
|
26
|
+
const matchedId = relatedMap.get(String(row.id));
|
|
27
|
+
if (matchedId) {
|
|
28
|
+
row._actionsData[action.relatedModel] = matchedId;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
Logger_1.default.error('mountCrudRoutes', `Error fetching related data for action ${action.label}:`, err);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
11
38
|
const utils_1 = require("./utils");
|
|
12
39
|
function mountCrudRoutes(ctx, modelNameLC) {
|
|
13
40
|
const { router } = ctx;
|
|
@@ -99,25 +126,62 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
99
126
|
const basePayload = {};
|
|
100
127
|
const childPayload = {};
|
|
101
128
|
for (const [k, v] of Object.entries(payload)) {
|
|
102
|
-
if (
|
|
129
|
+
if (k === 'baseId') {
|
|
130
|
+
childPayload[k] = v;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const baseAttr = baseSchema.attributes[k];
|
|
134
|
+
const childAttr = schema.attributes[k];
|
|
135
|
+
const bHead = Array.isArray(baseAttr) ? baseAttr[0] : baseAttr;
|
|
136
|
+
const cHead = Array.isArray(childAttr) ? childAttr[0] : childAttr;
|
|
137
|
+
if (bHead?.safe === true) {
|
|
103
138
|
basePayload[k] = v;
|
|
104
|
-
|
|
139
|
+
}
|
|
140
|
+
else if (cHead && !cHead.inherited) {
|
|
141
|
+
childPayload[k] = v;
|
|
142
|
+
}
|
|
143
|
+
else if (baseAttr) {
|
|
144
|
+
basePayload[k] = v;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
105
147
|
childPayload[k] = v;
|
|
148
|
+
}
|
|
106
149
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
150
|
+
const isUserExt = baseLC === 'users';
|
|
151
|
+
const baseId = payload.baseId;
|
|
152
|
+
let baseCreated;
|
|
153
|
+
if (isUserExt && baseId) {
|
|
154
|
+
// Use existing user
|
|
155
|
+
const arr = await baseModel.read({ id: baseId }, 1, 0, true);
|
|
156
|
+
baseCreated = arr?.[0];
|
|
157
|
+
if (!baseCreated) {
|
|
158
|
+
return res.status(400).json({ error: true, message: 'Associate user account not found.' });
|
|
159
|
+
}
|
|
110
160
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
161
|
+
else {
|
|
162
|
+
// Normal flow: create base record if we have enough data
|
|
163
|
+
const baseMissing = ctx.validateRequiredFields(baseSchema, basePayload, 'create');
|
|
164
|
+
// Only attempt creation if at least one base field is provided or if it's not a UserExt?
|
|
165
|
+
// Actually, for UserExt, if baseId is missing and they didn't fill user fields (which are hidden),
|
|
166
|
+
// we should skip base creation and just create the child.
|
|
167
|
+
if (baseMissing.length === 0 && Object.keys(basePayload).length > 0) {
|
|
168
|
+
if (baseLC === 'users' && basePayload.password && ctx.jwtSecret) {
|
|
169
|
+
const salt = await bcrypt_1.default.genSalt(10);
|
|
170
|
+
basePayload.password = await bcrypt_1.default.hash(String(basePayload.password) + ctx.jwtSecret, salt);
|
|
171
|
+
}
|
|
172
|
+
const baseConflicts = await ctx.checkUniqueFields(baseSchema, basePayload);
|
|
173
|
+
if (baseConflicts && baseConflicts.length) {
|
|
174
|
+
return res.status(400).json({
|
|
175
|
+
error: true,
|
|
176
|
+
fields: baseConflicts.map((field) => ({
|
|
177
|
+
field,
|
|
178
|
+
error: true,
|
|
179
|
+
message: `You cannot use this ${field}. It's already been used.`,
|
|
180
|
+
})),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
baseCreated = await baseModel.create(basePayload);
|
|
184
|
+
}
|
|
121
185
|
}
|
|
122
186
|
const childConflicts = await ctx.checkUniqueFields(schema, childPayload);
|
|
123
187
|
if (childConflicts && childConflicts.length) {
|
|
@@ -130,24 +194,35 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
130
194
|
})),
|
|
131
195
|
});
|
|
132
196
|
}
|
|
133
|
-
const
|
|
134
|
-
const childToCreate = { ...childPayload, baseId: baseCreated.id };
|
|
197
|
+
const childToCreate = { ...childPayload, baseId: baseCreated?.id || null };
|
|
135
198
|
const childCreated = await model.create(childToCreate);
|
|
136
|
-
let resultDoc =
|
|
137
|
-
|
|
138
|
-
|
|
199
|
+
let resultDoc = baseCreated
|
|
200
|
+
? { ...baseCreated, ...childCreated }
|
|
201
|
+
: { ...childCreated };
|
|
202
|
+
if (baseCreated) {
|
|
203
|
+
resultDoc.exId = String(baseCreated.id || baseCreated._id || '');
|
|
204
|
+
}
|
|
205
|
+
resultDoc.id = String(childCreated?.id || childCreated?._id || resultDoc.id || '');
|
|
139
206
|
resultDoc._id = resultDoc.id;
|
|
140
|
-
|
|
141
|
-
|
|
207
|
+
if (resultDoc.baseId)
|
|
208
|
+
resultDoc.baseId = String(resultDoc.baseId);
|
|
209
|
+
// delete resultDoc.baseId; // Preserve baseId for frontend use
|
|
142
210
|
if (cdec.exposePrivate && childCreated?.id) {
|
|
143
211
|
const [refChild] = await model.read({ id: childCreated.id }, 1, 0, true);
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
212
|
+
let refBase = null;
|
|
213
|
+
if (baseCreated) {
|
|
214
|
+
const [b] = await baseModel.read({ id: baseCreated.id }, 1, 0, true);
|
|
215
|
+
refBase = b;
|
|
216
|
+
}
|
|
217
|
+
if (refChild) {
|
|
218
|
+
resultDoc = refBase ? { ...refBase, ...refChild } : { ...refChild };
|
|
219
|
+
if (refBase)
|
|
220
|
+
resultDoc.exId = String(refBase.id ?? refBase._id);
|
|
148
221
|
resultDoc.id = String(refChild.id ?? refChild._id);
|
|
149
222
|
resultDoc._id = resultDoc.id;
|
|
150
|
-
|
|
223
|
+
if (resultDoc.baseId)
|
|
224
|
+
resultDoc.baseId = String(resultDoc.baseId);
|
|
225
|
+
// delete resultDoc.baseId; // Preserve baseId
|
|
151
226
|
}
|
|
152
227
|
}
|
|
153
228
|
const masked = cdec.exposePrivate
|
|
@@ -199,8 +274,8 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
199
274
|
.json({ error: true, message: 'Method not allowed' });
|
|
200
275
|
}
|
|
201
276
|
const model = ctx.getModel(modelNameLC);
|
|
202
|
-
const
|
|
203
|
-
const limit =
|
|
277
|
+
const baseKeys = new Set(Object.keys(schema.attributes || {}));
|
|
278
|
+
const { limit, page, skip, sort, projection } = (0, utils_1.parseQuery)(req, baseKeys);
|
|
204
279
|
const q = String(req.query.q ?? '').trim();
|
|
205
280
|
const searchKey = String(req.query.searchKey ?? '').trim();
|
|
206
281
|
const searchKeysCSV = String(req.query.searchKeys ?? '').trim();
|
|
@@ -210,127 +285,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
210
285
|
const dateFromStr = String(req.query.dateFrom ?? '').trim();
|
|
211
286
|
const dateToStr = String(req.query.dateTo ?? '').trim();
|
|
212
287
|
let dateKey = String(req.query.dateKey ?? 'createdAt').trim();
|
|
213
|
-
const
|
|
214
|
-
const sortTypeRaw = String(req.query.sortType ?? '').trim();
|
|
215
|
-
const filter = {};
|
|
216
|
-
// ---------- SEARCH ----------
|
|
217
|
-
const ors = [];
|
|
218
|
-
const ands = [];
|
|
219
|
-
if (q && searchKey && schema.attributes?.[searchKey]) {
|
|
220
|
-
const p = (0, utils_1.buildPredicateForField)(searchKey, schema.attributes[searchKey], q);
|
|
221
|
-
if (p)
|
|
222
|
-
ors.push(p);
|
|
223
|
-
}
|
|
224
|
-
if (q && searchKeysCSV) {
|
|
225
|
-
const keys = (0, utils_1.splitCSV)(searchKeysCSV);
|
|
226
|
-
const preds = keys
|
|
227
|
-
.filter((k) => !!schema.attributes?.[k])
|
|
228
|
-
.map((k) => (0, utils_1.buildPredicateForField)(k, schema.attributes[k], q))
|
|
229
|
-
.filter(Boolean);
|
|
230
|
-
if (preds.length) {
|
|
231
|
-
if (searchMode === 'and')
|
|
232
|
-
ands.push(...preds);
|
|
233
|
-
else
|
|
234
|
-
ors.push(...preds);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
if (ands.length && ors.length)
|
|
238
|
-
filter.$and = [...ands, { $or: ors }];
|
|
239
|
-
else if (ands.length)
|
|
240
|
-
filter.$and = [...(filter.$and || []), ...ands];
|
|
241
|
-
else if (ors.length)
|
|
242
|
-
filter.$or = [...(filter.$or || []), ...ors];
|
|
243
|
-
if (dateFromStr || dateToStr) {
|
|
244
|
-
if (!schema.attributes?.[dateKey]) {
|
|
245
|
-
if (schema.attributes?.createdAt)
|
|
246
|
-
dateKey = 'createdAt';
|
|
247
|
-
else if (schema.attributes?.updatedAt)
|
|
248
|
-
dateKey = 'updatedAt';
|
|
249
|
-
}
|
|
250
|
-
if (dateKey && schema.attributes?.[dateKey]) {
|
|
251
|
-
const range = {};
|
|
252
|
-
if (dateFromStr) {
|
|
253
|
-
const d = new Date(dateFromStr);
|
|
254
|
-
if (!Number.isNaN(+d))
|
|
255
|
-
range.$gte = d;
|
|
256
|
-
}
|
|
257
|
-
if (dateToStr) {
|
|
258
|
-
const d = new Date(dateToStr);
|
|
259
|
-
if (!Number.isNaN(+d))
|
|
260
|
-
range.$lte = d;
|
|
261
|
-
}
|
|
262
|
-
if (Object.keys(range).length)
|
|
263
|
-
filter[dateKey] = range;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
// ---------- Direct field filters from req.query ----------
|
|
267
|
-
const RESERVED = new Set([
|
|
268
|
-
'page',
|
|
269
|
-
'limit',
|
|
270
|
-
'q',
|
|
271
|
-
'searchKey',
|
|
272
|
-
'searchKeys',
|
|
273
|
-
'searchMode',
|
|
274
|
-
'dateFrom',
|
|
275
|
-
'dateTo',
|
|
276
|
-
'dateKey',
|
|
277
|
-
'sort',
|
|
278
|
-
'sortType',
|
|
279
|
-
]);
|
|
280
|
-
const attrMap = schema.attributes || {};
|
|
281
|
-
const coerceQueryVal = (raw) => {
|
|
282
|
-
const s = String(raw).trim();
|
|
283
|
-
if (/^(true|false)$/i.test(s))
|
|
284
|
-
return /^true$/i.test(s);
|
|
285
|
-
if (/^-?\d+(\.\d+)?$/.test(s))
|
|
286
|
-
return Number(s);
|
|
287
|
-
if (/^null$/i.test(s))
|
|
288
|
-
return null;
|
|
289
|
-
if (/^undefined$/i.test(s))
|
|
290
|
-
return undefined;
|
|
291
|
-
return s;
|
|
292
|
-
};
|
|
293
|
-
for (const [k, vRaw] of Object.entries(req.query)) {
|
|
294
|
-
if (RESERVED.has(k))
|
|
295
|
-
continue;
|
|
296
|
-
if (!Object.prototype.hasOwnProperty.call(attrMap, k))
|
|
297
|
-
continue;
|
|
298
|
-
const v = Array.isArray(vRaw)
|
|
299
|
-
? vRaw.map(String).join(',')
|
|
300
|
-
: String(vRaw);
|
|
301
|
-
if (!v?.length)
|
|
302
|
-
continue;
|
|
303
|
-
// Operators:
|
|
304
|
-
// - "a,b,c" -> $in
|
|
305
|
-
// - "!x" -> $ne
|
|
306
|
-
// - "gt:10" -> {$gt:10}, "gte:10","lt:5","lte:5"
|
|
307
|
-
// - default -> exact match
|
|
308
|
-
const tryNumOrBool = (s) => coerceQueryVal(s);
|
|
309
|
-
if (v.includes(',')) {
|
|
310
|
-
const arr = v
|
|
311
|
-
.split(',')
|
|
312
|
-
.map((s) => s.trim())
|
|
313
|
-
.filter(Boolean)
|
|
314
|
-
.map(tryNumOrBool);
|
|
315
|
-
filter[k] = { $in: arr };
|
|
316
|
-
}
|
|
317
|
-
else if (v.startsWith('!')) {
|
|
318
|
-
filter[k] = { $ne: tryNumOrBool(v.slice(1)) };
|
|
319
|
-
}
|
|
320
|
-
else if (/^(gt|gte|lt|lte):/i.test(v)) {
|
|
321
|
-
const [op, rest] = v.split(':', 2);
|
|
322
|
-
const map = {
|
|
323
|
-
gt: '$gt',
|
|
324
|
-
gte: '$gte',
|
|
325
|
-
lt: '$lt',
|
|
326
|
-
lte: '$lte',
|
|
327
|
-
};
|
|
328
|
-
filter[k] = { [map[op.toLowerCase()]]: tryNumOrBool(rest) };
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
filter[k] = tryNumOrBool(v);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
288
|
+
const filter = QueryEngine_1.QueryEngine.parse(req.query, schema);
|
|
334
289
|
const pctx = ctxFromReq(req);
|
|
335
290
|
const schemaPolicy = {
|
|
336
291
|
allowedMethods: schema.allowedMethods,
|
|
@@ -340,9 +295,6 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
340
295
|
if (!rdec.allow) {
|
|
341
296
|
return res.status(403).json({ error: true, message: 'forbidden' });
|
|
342
297
|
}
|
|
343
|
-
// ---------- SORT (updated; type-safe; honors client; allows virtual keys) ----------
|
|
344
|
-
const baseKeys = new Set(Object.keys(schema.attributes || {}));
|
|
345
|
-
const baseSort = (0, utils_1.parseSort)(sortRaw, sortTypeRaw, baseKeys);
|
|
346
298
|
const finalFilter = (0, authorize_1.andFilter)(filter, rdec.queryFilter || {});
|
|
347
299
|
const isUsersModel = modelNameLC === 'user' ||
|
|
348
300
|
modelNameLC === 'users' ||
|
|
@@ -355,40 +307,31 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
355
307
|
const baseSchema = ctx.getSchema(baseLC);
|
|
356
308
|
const baseModel = ctx.getModel(baseLC);
|
|
357
309
|
const baseAttrKeys = new Set(Object.keys(baseSchema.attributes || {}));
|
|
310
|
+
const childAttrs = schema.attributes || {};
|
|
311
|
+
for (const k of Object.keys(childAttrs)) {
|
|
312
|
+
const cHead = Array.isArray(childAttrs[k]) ? childAttrs[k][0] : childAttrs[k];
|
|
313
|
+
if (cHead && !cHead.inherited) {
|
|
314
|
+
baseAttrKeys.delete(k);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
358
317
|
const { child: childFilter, base: extBaseFilter } = (0, utils_1.splitFilterForExtended)(finalFilter, baseAttrKeys);
|
|
359
|
-
const { child: childSort, base: extBaseSort } = (0, utils_1.splitSortForExtended)(
|
|
318
|
+
const { child: childSort, base: extBaseSort } = (0, utils_1.splitSortForExtended)(sort, baseAttrKeys);
|
|
360
319
|
const needPrivateForHydrate = true;
|
|
361
320
|
const requiresBaseProcessing = Object.keys(extBaseFilter).length > 0 ||
|
|
362
321
|
Object.keys(extBaseSort).length > 0;
|
|
363
|
-
let
|
|
322
|
+
let dataRows = [];
|
|
364
323
|
if (requiresBaseProcessing) {
|
|
365
|
-
|
|
324
|
+
// 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 });
|
|
366
326
|
}
|
|
367
327
|
else {
|
|
368
|
-
|
|
328
|
+
// Normal paginated read (already hydrated by adapter)
|
|
329
|
+
dataRows = await model.read(childFilter, limit, skip, needPrivateForHydrate, {
|
|
330
|
+
sort: Object.keys(childSort).length ? childSort : undefined,
|
|
331
|
+
projection,
|
|
332
|
+
});
|
|
369
333
|
}
|
|
370
|
-
|
|
371
|
-
const baseIds = Array.from(new Set(childRows
|
|
372
|
-
.map((r) => (0, utils_1.toIdString)(r?.baseId))
|
|
373
|
-
.filter((s) => !!s)));
|
|
374
|
-
const baseDocs = baseIds.length
|
|
375
|
-
? await baseModel.read({ id: { $in: baseIds } }, baseIds.length, 0, !!rdec.exposePrivate)
|
|
376
|
-
: [];
|
|
377
|
-
const baseMap = new Map(baseDocs.map((b) => [String(b.id), b]));
|
|
378
|
-
// merge, but force merged id to BASE id (single entity identity)
|
|
379
|
-
let merged = childRows.map((row) => {
|
|
380
|
-
const bid = (0, utils_1.toIdString)(row?.baseId);
|
|
381
|
-
const base = bid ? baseMap.get(bid) : null;
|
|
382
|
-
const m = base ? { ...base, ...row } : { ...row };
|
|
383
|
-
// expose base id as exId, keep child's id as id
|
|
384
|
-
m.exId = String(base?.id ?? base?._id ?? bid ?? '');
|
|
385
|
-
m.id = String(row?.id ?? row?._id);
|
|
386
|
-
m._id = m.id;
|
|
387
|
-
// optional: keep for trace/debug
|
|
388
|
-
m.__childId = m.id;
|
|
389
|
-
delete m.baseId;
|
|
390
|
-
return m;
|
|
391
|
-
});
|
|
334
|
+
let merged = dataRows;
|
|
392
335
|
// apply base-level filters/sort, then DEDUPE by canonical id
|
|
393
336
|
if (Object.keys(extBaseFilter).length) {
|
|
394
337
|
merged = merged.filter((m) => (0, utils_1.matchDoc)(m, extBaseFilter));
|
|
@@ -396,10 +339,18 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
396
339
|
const combinedSort = { ...extBaseSort, ...childSort };
|
|
397
340
|
if (Object.keys(combinedSort).length) {
|
|
398
341
|
merged = merged.sort((a, b) => {
|
|
342
|
+
const getField = (obj, k) => {
|
|
343
|
+
if (obj?.[k] !== undefined)
|
|
344
|
+
return obj[k];
|
|
345
|
+
if (obj?.baseId && typeof obj.baseId === 'object' && obj.baseId[k] !== undefined) {
|
|
346
|
+
return obj.baseId[k];
|
|
347
|
+
}
|
|
348
|
+
return undefined;
|
|
349
|
+
};
|
|
399
350
|
for (const k of Object.keys(combinedSort)) {
|
|
400
351
|
const dir = combinedSort[k];
|
|
401
|
-
const av = a
|
|
402
|
-
const bv = b
|
|
352
|
+
const av = getField(a, k);
|
|
353
|
+
const bv = getField(b, k);
|
|
403
354
|
const ax = av instanceof Date ? +av : av;
|
|
404
355
|
const bx = bv instanceof Date ? +bv : bv;
|
|
405
356
|
if (ax == null && bx == null)
|
|
@@ -426,7 +377,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
426
377
|
let paged;
|
|
427
378
|
if (requiresBaseProcessing) {
|
|
428
379
|
totalRows = merged.length;
|
|
429
|
-
const start =
|
|
380
|
+
const start = skip;
|
|
430
381
|
paged = merged.slice(start, start + limit);
|
|
431
382
|
}
|
|
432
383
|
else {
|
|
@@ -437,6 +388,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
437
388
|
const data = rdec.exposePrivate
|
|
438
389
|
? (0, authorize_1.applyReadMaskMany)(paged, rdec.sensitiveMask)
|
|
439
390
|
: (0, authorize_1.applyReadMaskMany)(paged, rdec.readMask);
|
|
391
|
+
await injectActionRelatedData(ctx, schema, data);
|
|
440
392
|
return res.status(200).json({
|
|
441
393
|
success: true,
|
|
442
394
|
message: `Data fetched for ${schema.modelName}`,
|
|
@@ -446,15 +398,14 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
446
398
|
});
|
|
447
399
|
}
|
|
448
400
|
// ---------- NON-EXTENDED ----------
|
|
449
|
-
const sort = baseSort;
|
|
450
401
|
const totalRows = await model.count(finalFilter);
|
|
451
402
|
let rawRows = [];
|
|
452
403
|
const exposePrivateForRead = !!rdec.exposePrivate;
|
|
453
404
|
try {
|
|
454
|
-
rawRows = await model.read(finalFilter, limit + 1,
|
|
405
|
+
rawRows = await model.read(finalFilter, limit + 1, skip, exposePrivateForRead, { sort, projection });
|
|
455
406
|
}
|
|
456
407
|
catch {
|
|
457
|
-
rawRows = await model.read(finalFilter, limit + 1,
|
|
408
|
+
rawRows = await model.read(finalFilter, limit + 1, skip, exposePrivateForRead);
|
|
458
409
|
if (sort && Object.keys(sort).length) {
|
|
459
410
|
const orderKeys = Object.keys(sort);
|
|
460
411
|
rawRows.sort((a, b) => {
|
|
@@ -487,6 +438,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
487
438
|
const data = rdec.exposePrivate
|
|
488
439
|
? (0, authorize_1.applyReadMaskMany)(rows, rdec.sensitiveMask)
|
|
489
440
|
: (0, authorize_1.applyReadMaskMany)(rows, rdec.readMask);
|
|
441
|
+
await injectActionRelatedData(ctx, schema, data);
|
|
490
442
|
return res.status(200).json({
|
|
491
443
|
success: true,
|
|
492
444
|
message: `Data fetched for ${schema.modelName}`,
|
|
@@ -512,6 +464,8 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
512
464
|
.json({ error: true, message: 'Method not allowed' });
|
|
513
465
|
}
|
|
514
466
|
const model = ctx.getModel(modelNameLC);
|
|
467
|
+
const baseKeys = new Set(Object.keys(schema.attributes || {}));
|
|
468
|
+
const { projection } = (0, utils_1.parseQuery)(req, baseKeys);
|
|
515
469
|
const pctx = ctxFromReq(req);
|
|
516
470
|
const schemaPolicy = {
|
|
517
471
|
allowedMethods: schema.allowedMethods,
|
|
@@ -520,7 +474,16 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
520
474
|
const decForInclude = (0, authorize_1.authorize)(modelNameLC, 'read', schemaPolicy, pctx);
|
|
521
475
|
const needPrivateForHydrate = !!schema.extends;
|
|
522
476
|
const exposePrivateForRead = needPrivateForHydrate || !!decForInclude.exposePrivate;
|
|
523
|
-
const recordArr = await model.read({ id: req.params.id }, 1, 0, exposePrivateForRead
|
|
477
|
+
const recordArr = await model.read({ id: req.params.id }, 1, 0, exposePrivateForRead, {
|
|
478
|
+
projection: (() => {
|
|
479
|
+
if (!projection)
|
|
480
|
+
return undefined;
|
|
481
|
+
if (!schema.extends)
|
|
482
|
+
return projection;
|
|
483
|
+
// For extended schemas, ensure baseId is included for hydration
|
|
484
|
+
return { ...projection, baseId: 1 };
|
|
485
|
+
})()
|
|
486
|
+
});
|
|
524
487
|
const doc = recordArr?.[0];
|
|
525
488
|
if (!doc) {
|
|
526
489
|
return res
|
|
@@ -532,22 +495,20 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
532
495
|
return res.status(403).json({ error: true, message: 'forbidden' });
|
|
533
496
|
let toReturn = doc;
|
|
534
497
|
if (schema.extends) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
if (baseDoc) {
|
|
542
|
-
toReturn = { ...baseDoc, ...doc };
|
|
543
|
-
toReturn.exId = String(baseDoc.id ?? baseDoc._id ?? baseIdStr);
|
|
544
|
-
toReturn.id = String(doc.id ?? doc._id);
|
|
545
|
-
toReturn._id = toReturn.id;
|
|
546
|
-
delete toReturn.baseId;
|
|
498
|
+
// Hydration is handled by adapter.
|
|
499
|
+
// We just ensure exId and baseId consistency if hydrated.
|
|
500
|
+
if (doc) {
|
|
501
|
+
toReturn = { ...doc };
|
|
502
|
+
if (!toReturn.exId && toReturn.baseId) {
|
|
503
|
+
toReturn.exId = (0, utils_1.toIdString)(toReturn.baseId);
|
|
547
504
|
}
|
|
505
|
+
if (toReturn.id)
|
|
506
|
+
toReturn.id = String(toReturn.id);
|
|
507
|
+
if (toReturn._id)
|
|
508
|
+
toReturn._id = String(toReturn._id);
|
|
509
|
+
if (toReturn.baseId)
|
|
510
|
+
toReturn.baseId = String(toReturn.baseId);
|
|
548
511
|
}
|
|
549
|
-
if (toReturn)
|
|
550
|
-
delete toReturn.baseId;
|
|
551
512
|
}
|
|
552
513
|
const data = dec.exposePrivate
|
|
553
514
|
? (0, authorize_1.applyReadMaskOne)(toReturn, dec.sensitiveMask)
|
|
@@ -614,25 +575,36 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
614
575
|
const baseLC = baseName.toLowerCase();
|
|
615
576
|
const baseSchema = ctx.getSchema(baseLC);
|
|
616
577
|
const baseModel = ctx.getModel(baseLC);
|
|
617
|
-
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
return res.status(400).json({
|
|
621
|
-
error: true,
|
|
622
|
-
message: 'Invalid extended record: missing baseId',
|
|
623
|
-
});
|
|
624
|
-
}
|
|
578
|
+
// Determine target baseId: prefer payload if changed, otherwise current
|
|
579
|
+
const targetBaseIdRaw = upd.baseId !== undefined ? upd.baseId : before?.baseId;
|
|
580
|
+
const targetBaseId = (0, utils_1.toIdString)(targetBaseIdRaw);
|
|
625
581
|
const baseUpd = {};
|
|
626
582
|
const childUpd = {};
|
|
627
583
|
for (const [k, v] of Object.entries(upd)) {
|
|
628
|
-
if (k === 'baseId')
|
|
584
|
+
if (k === 'baseId') {
|
|
585
|
+
childUpd[k] = v;
|
|
629
586
|
continue;
|
|
630
|
-
|
|
587
|
+
}
|
|
588
|
+
const baseAttr = baseSchema.attributes[k];
|
|
589
|
+
const childAttr = schema.attributes[k];
|
|
590
|
+
const bHead = Array.isArray(baseAttr) ? baseAttr[0] : baseAttr;
|
|
591
|
+
const cHead = Array.isArray(childAttr) ? childAttr[0] : childAttr;
|
|
592
|
+
if (bHead?.safe === true) {
|
|
631
593
|
baseUpd[k] = v;
|
|
632
|
-
|
|
594
|
+
}
|
|
595
|
+
else if (cHead && !cHead.inherited) {
|
|
633
596
|
childUpd[k] = v;
|
|
597
|
+
}
|
|
598
|
+
else if (baseAttr) {
|
|
599
|
+
baseUpd[k] = v;
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
childUpd[k] = v;
|
|
603
|
+
}
|
|
634
604
|
}
|
|
635
|
-
const baseConflicts =
|
|
605
|
+
const baseConflicts = targetBaseId
|
|
606
|
+
? await ctx.checkUniqueFields(baseSchema, baseUpd, targetBaseId || undefined)
|
|
607
|
+
: null;
|
|
636
608
|
if (baseConflicts && baseConflicts.length) {
|
|
637
609
|
return res.status(400).json({
|
|
638
610
|
error: true,
|
|
@@ -670,17 +642,32 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
670
642
|
if (Object.keys(childUpd).length) {
|
|
671
643
|
updatedChild = await model.update(req.params.id, childUpd);
|
|
672
644
|
}
|
|
673
|
-
if (Object.keys(baseUpd).length) {
|
|
674
|
-
await baseModel.update(
|
|
645
|
+
if (targetBaseId && Object.keys(baseUpd).length) {
|
|
646
|
+
await baseModel.update(targetBaseId, baseUpd);
|
|
647
|
+
}
|
|
648
|
+
const [refChild] = await model.read({ id: String(req.params.id) }, 1, 0, true);
|
|
649
|
+
const refBase = targetBaseId
|
|
650
|
+
? (await baseModel.read({ id: targetBaseId }, 1, 0, true))?.[0]
|
|
651
|
+
: null;
|
|
652
|
+
let responseDoc = refChild ? { ...refChild } : { ...updatedChild, id: req.params.id };
|
|
653
|
+
if (refChild && refBase) {
|
|
654
|
+
// Overlap logic: prefer child value if it's set/not-empty
|
|
655
|
+
responseDoc = { ...refBase };
|
|
656
|
+
for (const k of Object.keys(refChild)) {
|
|
657
|
+
const val = refChild[k];
|
|
658
|
+
const hasValue = val != null && (!Array.isArray(val) || val.length > 0);
|
|
659
|
+
if (hasValue) {
|
|
660
|
+
responseDoc[k] = val;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
675
663
|
}
|
|
676
|
-
const [refChild] = await model.read({ id: String(updatedChild.id) }, 1, 0, true);
|
|
677
|
-
const [refBase] = await baseModel.read({ id: baseId }, 1, 0, true);
|
|
678
|
-
let responseDoc = refChild && refBase ? { ...refBase, ...refChild } : updatedChild;
|
|
679
664
|
if (responseDoc) {
|
|
680
|
-
responseDoc.exId = String(
|
|
681
|
-
responseDoc.id = String(
|
|
665
|
+
responseDoc.exId = String(targetBaseId || '');
|
|
666
|
+
responseDoc.id = String(req.params.id);
|
|
682
667
|
responseDoc._id = responseDoc.id;
|
|
683
|
-
|
|
668
|
+
if (responseDoc.baseId)
|
|
669
|
+
responseDoc.baseId = String(responseDoc.baseId);
|
|
670
|
+
// delete (responseDoc as any).baseId;
|
|
684
671
|
}
|
|
685
672
|
const masked = udec.exposePrivate
|
|
686
673
|
? (0, authorize_1.applyReadMaskOne)(responseDoc, udec.sensitiveMask)
|
|
@@ -83,31 +83,7 @@ function mountFindRoutes(ctx) {
|
|
|
83
83
|
else {
|
|
84
84
|
items = await ctx.getModel(targetLC).read({ id: { $in: ids } }, limit, skip, !!tDec.exposePrivate);
|
|
85
85
|
}
|
|
86
|
-
// extended hydration
|
|
87
|
-
if (targetSchema.extends && Array.isArray(items) && items.length) {
|
|
88
|
-
const baseName = String(targetSchema.extends);
|
|
89
|
-
const baseLC = baseName.toLowerCase();
|
|
90
|
-
const baseModel = ctx.getModel(baseLC);
|
|
91
|
-
const baseIds = Array.from(new Set(items.map((it) => it?.baseId).filter(Boolean)));
|
|
92
|
-
if (baseIds.length) {
|
|
93
|
-
const baseDocs = await baseModel.read({ id: { $in: baseIds } }, baseIds.length, 0, !!tDec.exposePrivate);
|
|
94
|
-
const baseMap = new Map(baseDocs.map((b) => [String(b?.id ?? b?._id), b]));
|
|
95
|
-
items = items.map((it) => {
|
|
96
|
-
const bid = String(it?.baseId || "");
|
|
97
|
-
const b = bid ? baseMap.get(bid) : null;
|
|
98
|
-
const merged = b ? { ...b, ...it } : { ...it };
|
|
99
|
-
delete merged.baseId;
|
|
100
|
-
return merged;
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
items = items.map((it) => {
|
|
105
|
-
const copy = { ...it };
|
|
106
|
-
delete copy.baseId;
|
|
107
|
-
return copy;
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
86
|
+
// extended hydration is handled by adapter.read/findMany
|
|
111
87
|
const masked = tDec.exposePrivate ? (0, authorize_1.applyReadMaskMany)(items, tDec.sensitiveMask) : (0, authorize_1.applyReadMaskMany)(items, tDec.readMask);
|
|
112
88
|
const totalRows = ids.length;
|
|
113
89
|
return res.status(200).json({
|
|
@@ -163,30 +139,7 @@ function mountFindRoutes(ctx) {
|
|
|
163
139
|
totalRows = all.length;
|
|
164
140
|
items = all.slice(skip, skip + limit);
|
|
165
141
|
}
|
|
166
|
-
|
|
167
|
-
const baseName = String(targetSchema.extends);
|
|
168
|
-
const baseLC = baseName.toLowerCase();
|
|
169
|
-
const baseModel = ctx.getModel(baseLC);
|
|
170
|
-
const baseIds = Array.from(new Set(items.map((it) => it?.baseId).filter(Boolean)));
|
|
171
|
-
if (baseIds.length) {
|
|
172
|
-
const baseDocs = await baseModel.read({ id: { $in: baseIds } }, baseIds.length, 0, !!tDec.exposePrivate);
|
|
173
|
-
const baseMap = new Map(baseDocs.map((b) => [String(b?.id ?? b?._id), b]));
|
|
174
|
-
items = items.map((it) => {
|
|
175
|
-
const bid = String(it?.baseId || "");
|
|
176
|
-
const b = bid ? baseMap.get(bid) : null;
|
|
177
|
-
const merged = b ? { ...b, ...it } : { ...it };
|
|
178
|
-
delete merged.baseId;
|
|
179
|
-
return merged;
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
items = items.map((it) => {
|
|
184
|
-
const copy = { ...it };
|
|
185
|
-
delete copy.baseId;
|
|
186
|
-
return copy;
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
142
|
+
// extended hydration is handled by adapter.read/findMany
|
|
190
143
|
const masked = tDec.exposePrivate ? (0, authorize_1.applyReadMaskMany)(items, tDec.sensitiveMask) : (0, authorize_1.applyReadMaskMany)(items, tDec.readMask);
|
|
191
144
|
return res.status(200).json({
|
|
192
145
|
success: true,
|