@airoom/nextmin-node 0.1.6 → 0.1.8
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.
|
@@ -134,12 +134,19 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
134
134
|
const childToCreate = { ...childPayload, baseId: baseCreated.id };
|
|
135
135
|
const childCreated = await model.create(childToCreate);
|
|
136
136
|
let resultDoc = { ...baseCreated, ...childCreated };
|
|
137
|
+
resultDoc.exId = String(baseCreated.id);
|
|
138
|
+
resultDoc.id = String(childCreated.id);
|
|
139
|
+
resultDoc._id = resultDoc.id;
|
|
140
|
+
delete resultDoc.baseId;
|
|
137
141
|
delete resultDoc.baseId;
|
|
138
142
|
if (cdec.exposePrivate && childCreated?.id) {
|
|
139
143
|
const [refChild] = await model.read({ id: childCreated.id }, 1, 0, true);
|
|
140
144
|
const [refBase] = await baseModel.read({ id: baseCreated.id }, 1, 0, true);
|
|
141
145
|
if (refChild && refBase) {
|
|
142
146
|
resultDoc = { ...refBase, ...refChild };
|
|
147
|
+
resultDoc.exId = String(refBase.id ?? refBase._id);
|
|
148
|
+
resultDoc.id = String(refChild.id ?? refChild._id);
|
|
149
|
+
resultDoc._id = resultDoc.id;
|
|
143
150
|
delete resultDoc.baseId;
|
|
144
151
|
}
|
|
145
152
|
}
|
|
@@ -370,10 +377,9 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
370
377
|
childRows = await model.read(childFilter, 0, 0, needPrivateForHydrate);
|
|
371
378
|
}
|
|
372
379
|
else {
|
|
373
|
-
childRows = await model.read(childFilter, limit, page * limit, needPrivateForHydrate, {
|
|
374
|
-
sort: Object.keys(childSort).length ? childSort : undefined,
|
|
375
|
-
});
|
|
380
|
+
childRows = await model.read(childFilter, limit, page * limit, needPrivateForHydrate, { sort: Object.keys(childSort).length ? childSort : undefined });
|
|
376
381
|
}
|
|
382
|
+
// build base map
|
|
377
383
|
const baseIds = Array.from(new Set(childRows
|
|
378
384
|
.map((r) => (0, utils_1.toIdString)(r?.baseId))
|
|
379
385
|
.filter((s) => !!s)));
|
|
@@ -381,13 +387,21 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
381
387
|
? await baseModel.read({ id: { $in: baseIds } }, baseIds.length, 0, !!rdec.exposePrivate)
|
|
382
388
|
: [];
|
|
383
389
|
const baseMap = new Map(baseDocs.map((b) => [String(b.id), b]));
|
|
390
|
+
// merge, but force merged id to BASE id (single entity identity)
|
|
384
391
|
let merged = childRows.map((row) => {
|
|
385
392
|
const bid = (0, utils_1.toIdString)(row?.baseId);
|
|
386
|
-
const
|
|
387
|
-
const m =
|
|
393
|
+
const base = bid ? baseMap.get(bid) : null;
|
|
394
|
+
const m = base ? { ...base, ...row } : { ...row };
|
|
395
|
+
// expose base id as exId, keep child's id as id
|
|
396
|
+
m.exId = String(base?.id ?? base?._id ?? bid ?? '');
|
|
397
|
+
m.id = String(row?.id ?? row?._id);
|
|
398
|
+
m._id = m.id;
|
|
399
|
+
// optional: keep for trace/debug
|
|
400
|
+
m.__childId = m.id;
|
|
388
401
|
delete m.baseId;
|
|
389
402
|
return m;
|
|
390
403
|
});
|
|
404
|
+
// apply base-level filters/sort, then DEDUPE by canonical id
|
|
391
405
|
if (Object.keys(extBaseFilter).length) {
|
|
392
406
|
merged = merged.filter((m) => (0, utils_1.matchDoc)(m, extBaseFilter));
|
|
393
407
|
}
|
|
@@ -395,9 +409,12 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
395
409
|
if (Object.keys(combinedSort).length) {
|
|
396
410
|
merged = (0, utils_1.sortInMemory)(merged, combinedSort);
|
|
397
411
|
}
|
|
412
|
+
// **dedupe**: one row per base entity
|
|
413
|
+
merged = (0, utils_1.dedupeBy)(merged, (r) => String(r.exId || r.id || r._id || ''));
|
|
398
414
|
if (currentUserId && isUsersModel) {
|
|
399
415
|
merged = merged.filter((r) => String(r?.id) !== String(currentUserId));
|
|
400
416
|
}
|
|
417
|
+
// pagination + totals
|
|
401
418
|
let totalRows;
|
|
402
419
|
let paged;
|
|
403
420
|
if (requiresBaseProcessing) {
|
|
@@ -406,7 +423,9 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
406
423
|
paged = merged.slice(start, start + limit);
|
|
407
424
|
}
|
|
408
425
|
else {
|
|
409
|
-
|
|
426
|
+
// If your model supports it, prefer a distinct count on baseId:
|
|
427
|
+
// totalRows = await (model as any).countDistinct?.('baseId', childFilter) ?? merged.length;
|
|
428
|
+
totalRows = merged.length; // fallback keeps UI consistent with deduped list
|
|
410
429
|
paged = merged.slice(0, limit);
|
|
411
430
|
}
|
|
412
431
|
const data = rdec.exposePrivate
|
|
@@ -515,6 +534,10 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
515
534
|
const [baseDoc] = await baseModel.read({ id: baseIdStr }, 1, 0, !!dec.exposePrivate);
|
|
516
535
|
if (baseDoc) {
|
|
517
536
|
toReturn = { ...baseDoc, ...doc };
|
|
537
|
+
toReturn.exId = String(baseDoc.id ?? baseDoc._id ?? baseIdStr);
|
|
538
|
+
toReturn.id = String(doc.id ?? doc._id);
|
|
539
|
+
toReturn._id = toReturn.id;
|
|
540
|
+
delete toReturn.baseId;
|
|
518
541
|
}
|
|
519
542
|
}
|
|
520
543
|
if (toReturn)
|
|
@@ -647,8 +670,12 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
647
670
|
const [refChild] = await model.read({ id: String(updatedChild.id) }, 1, 0, true);
|
|
648
671
|
const [refBase] = await baseModel.read({ id: baseId }, 1, 0, true);
|
|
649
672
|
let responseDoc = refChild && refBase ? { ...refBase, ...refChild } : updatedChild;
|
|
650
|
-
if (responseDoc)
|
|
673
|
+
if (responseDoc) {
|
|
674
|
+
responseDoc.exId = String(baseId);
|
|
675
|
+
responseDoc.id = String((refChild?.id ?? updatedChild?.id));
|
|
676
|
+
responseDoc._id = responseDoc.id;
|
|
651
677
|
delete responseDoc.baseId;
|
|
678
|
+
}
|
|
652
679
|
const masked = udec.exposePrivate
|
|
653
680
|
? (0, authorize_1.applyReadMaskOne)(responseDoc, udec.sensitiveMask)
|
|
654
681
|
: (0, authorize_1.applyReadMaskOne)(responseDoc, udec.readMask);
|
|
@@ -719,7 +746,7 @@ function mountCrudRoutes(ctx, modelNameLC) {
|
|
|
719
746
|
catch { }
|
|
720
747
|
}
|
|
721
748
|
const merged = baseId
|
|
722
|
-
? { ...deletedChild, baseId: undefined }
|
|
749
|
+
? { ...deletedChild, exId: String(baseId), baseId: undefined }
|
|
723
750
|
: deletedChild;
|
|
724
751
|
const masked = ddec.exposePrivate
|
|
725
752
|
? (0, authorize_1.applyReadMaskOne)(merged, ddec.sensitiveMask)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Request } from
|
|
1
|
+
import type { Request } from 'express';
|
|
2
2
|
export type AnyRec = Record<string, any>;
|
|
3
3
|
export type SortDir = 1 | -1;
|
|
4
4
|
export type SortSpec = Record<string, SortDir>;
|
|
@@ -61,3 +61,4 @@ export declare function refInfoFromAttr(attr: any): {
|
|
|
61
61
|
ref: string;
|
|
62
62
|
isArray: boolean;
|
|
63
63
|
} | null;
|
|
64
|
+
export declare const dedupeBy: <T, K extends string | number>(rows: T[], key: (r: T) => K) => T[];
|
package/dist/api/router/utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.splitCSV = exports.isPlainObject = void 0;
|
|
3
|
+
exports.dedupeBy = exports.splitCSV = exports.isPlainObject = void 0;
|
|
4
4
|
exports.normalizeAttrType = normalizeAttrType;
|
|
5
5
|
exports.toIdString = toIdString;
|
|
6
6
|
exports.splitFilterForExtended = splitFilterForExtended;
|
|
@@ -12,38 +12,41 @@ exports.parseSort = parseSort;
|
|
|
12
12
|
exports.parseQuery = parseQuery;
|
|
13
13
|
exports.extractIds = extractIds;
|
|
14
14
|
exports.refInfoFromAttr = refInfoFromAttr;
|
|
15
|
-
const isPlainObject = (v) => !!v && typeof v ===
|
|
15
|
+
const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
16
16
|
exports.isPlainObject = isPlainObject;
|
|
17
|
-
const splitCSV = (raw) => raw
|
|
17
|
+
const splitCSV = (raw) => raw
|
|
18
|
+
.split(',')
|
|
19
|
+
.map((s) => s.trim())
|
|
20
|
+
.filter(Boolean);
|
|
18
21
|
exports.splitCSV = splitCSV;
|
|
19
22
|
function normalizeAttrType(attr) {
|
|
20
23
|
const a = Array.isArray(attr) ? attr?.[0] : attr;
|
|
21
24
|
let t = a?.type ?? a;
|
|
22
|
-
if (typeof t ===
|
|
25
|
+
if (typeof t === 'function' && t.name)
|
|
23
26
|
t = t.name;
|
|
24
|
-
if (t && typeof t ===
|
|
27
|
+
if (t && typeof t === 'object' && 'name' in t)
|
|
25
28
|
t = t.name;
|
|
26
|
-
if (typeof t ===
|
|
29
|
+
if (typeof t === 'string')
|
|
27
30
|
t = t.toLowerCase();
|
|
28
|
-
if (t ===
|
|
29
|
-
t =
|
|
30
|
-
if (t ===
|
|
31
|
-
t =
|
|
32
|
-
return String(t ||
|
|
31
|
+
if (t === 'bool')
|
|
32
|
+
t = 'boolean';
|
|
33
|
+
if (t === 'objectid' || t === 'oid' || t === 'ref')
|
|
34
|
+
t = 'objectid';
|
|
35
|
+
return String(t || '');
|
|
33
36
|
}
|
|
34
37
|
function toIdString(v) {
|
|
35
38
|
if (!v)
|
|
36
39
|
return null;
|
|
37
|
-
if (typeof v ===
|
|
40
|
+
if (typeof v === 'string')
|
|
38
41
|
return v;
|
|
39
|
-
if (typeof v ===
|
|
42
|
+
if (typeof v === 'number')
|
|
40
43
|
return String(v);
|
|
41
|
-
if (typeof v ===
|
|
42
|
-
if (typeof v.id ===
|
|
44
|
+
if (typeof v === 'object') {
|
|
45
|
+
if (typeof v.id === 'string')
|
|
43
46
|
return v.id;
|
|
44
|
-
if (v._id && typeof v._id.toString ===
|
|
47
|
+
if (v._id && typeof v._id.toString === 'function')
|
|
45
48
|
return v._id.toString();
|
|
46
|
-
if (typeof v._id ===
|
|
49
|
+
if (typeof v._id === 'string')
|
|
47
50
|
return v._id;
|
|
48
51
|
}
|
|
49
52
|
return null;
|
|
@@ -55,7 +58,7 @@ function splitFilterForExtended(filter, baseKeys) {
|
|
|
55
58
|
const outChild = {};
|
|
56
59
|
const outBase = {};
|
|
57
60
|
for (const [k, v] of Object.entries(node)) {
|
|
58
|
-
if (k ===
|
|
61
|
+
if (k === '$and' || k === '$or' || k === '$nor') {
|
|
59
62
|
if (!Array.isArray(v))
|
|
60
63
|
continue;
|
|
61
64
|
const childArr = [];
|
|
@@ -102,8 +105,8 @@ function sortInMemory(rows, sort) {
|
|
|
102
105
|
const dir = sort[k];
|
|
103
106
|
const av = a?.[k];
|
|
104
107
|
const bv = b?.[k];
|
|
105
|
-
const ax = av instanceof Date ? +av : (av ??
|
|
106
|
-
const bx = bv instanceof Date ? +bv : (bv ??
|
|
108
|
+
const ax = av instanceof Date ? +av : (av ?? '');
|
|
109
|
+
const bx = bv instanceof Date ? +bv : (bv ?? '');
|
|
107
110
|
if (ax > bx)
|
|
108
111
|
return dir;
|
|
109
112
|
if (ax < bx)
|
|
@@ -115,23 +118,23 @@ function sortInMemory(rows, sort) {
|
|
|
115
118
|
function matchDoc(doc, filter) {
|
|
116
119
|
const evalNode = (node) => {
|
|
117
120
|
for (const [k, v] of Object.entries(node)) {
|
|
118
|
-
if (k ===
|
|
121
|
+
if (k === '$and' && Array.isArray(v))
|
|
119
122
|
return v.every((n) => evalNode(n));
|
|
120
|
-
if (k ===
|
|
123
|
+
if (k === '$or' && Array.isArray(v))
|
|
121
124
|
return v.some((n) => evalNode(n));
|
|
122
|
-
if (k ===
|
|
125
|
+
if (k === '$nor' && Array.isArray(v))
|
|
123
126
|
return !v.some((n) => evalNode(n));
|
|
124
127
|
const dv = doc[k];
|
|
125
128
|
if ((0, exports.isPlainObject)(v)) {
|
|
126
|
-
if (
|
|
129
|
+
if ('$in' in v && !v.$in.includes(dv))
|
|
127
130
|
return false;
|
|
128
|
-
if (
|
|
131
|
+
if ('$gte' in v && !(dv >= v.$gte))
|
|
129
132
|
return false;
|
|
130
|
-
if (
|
|
133
|
+
if ('$lte' in v && !(dv <= v.$lte))
|
|
131
134
|
return false;
|
|
132
|
-
if (
|
|
133
|
-
const re = new RegExp(v.$regex, v.$options ||
|
|
134
|
-
if (!re.test(String(dv ??
|
|
135
|
+
if ('$regex' in v) {
|
|
136
|
+
const re = new RegExp(v.$regex, v.$options || '');
|
|
137
|
+
if (!re.test(String(dv ?? '')))
|
|
135
138
|
return false;
|
|
136
139
|
}
|
|
137
140
|
}
|
|
@@ -150,24 +153,42 @@ function buildPredicateForField(field, attr, raw) {
|
|
|
150
153
|
const attrType = normalizeAttrType(base);
|
|
151
154
|
const tokens = (0, exports.splitCSV)(raw);
|
|
152
155
|
switch (attrType) {
|
|
153
|
-
case
|
|
154
|
-
return isArray
|
|
155
|
-
|
|
156
|
+
case 'string':
|
|
157
|
+
return isArray
|
|
158
|
+
? { [field]: { $in: tokens.length ? tokens : [raw] } }
|
|
159
|
+
: { [field]: { $regex: raw, $options: 'i' } };
|
|
160
|
+
case 'number': {
|
|
156
161
|
const nums = tokens.map(Number).filter((n) => !Number.isNaN(n));
|
|
157
|
-
return isArray
|
|
162
|
+
return isArray
|
|
163
|
+
? nums.length
|
|
164
|
+
? { [field]: { $in: nums } }
|
|
165
|
+
: null
|
|
166
|
+
: nums.length
|
|
167
|
+
? { [field]: nums[0] }
|
|
168
|
+
: null;
|
|
158
169
|
}
|
|
159
|
-
case
|
|
160
|
-
const toBool = (t) => /^(true|1|yes)$/i.test(t)
|
|
170
|
+
case 'boolean': {
|
|
171
|
+
const toBool = (t) => /^(true|1|yes)$/i.test(t)
|
|
172
|
+
? true
|
|
173
|
+
: /^(false|0|no)$/i.test(t)
|
|
174
|
+
? false
|
|
175
|
+
: null;
|
|
161
176
|
if (isArray) {
|
|
162
|
-
const bools = tokens
|
|
177
|
+
const bools = tokens
|
|
178
|
+
.map(toBool)
|
|
179
|
+
.filter((v) => v !== null);
|
|
163
180
|
return bools.length ? { [field]: { $in: bools } } : null;
|
|
164
181
|
}
|
|
165
182
|
const b = toBool(raw);
|
|
166
183
|
return b === null ? null : { [field]: b };
|
|
167
184
|
}
|
|
168
|
-
case
|
|
169
|
-
return isArray || tokens.length > 1
|
|
170
|
-
|
|
185
|
+
case 'objectid':
|
|
186
|
+
return isArray || tokens.length > 1
|
|
187
|
+
? { [field]: { $in: tokens } }
|
|
188
|
+
: raw
|
|
189
|
+
? { [field]: raw }
|
|
190
|
+
: null;
|
|
191
|
+
case 'date': {
|
|
171
192
|
const toDate = (t) => {
|
|
172
193
|
const d = new Date(t);
|
|
173
194
|
return Number.isNaN(+d) ? null : d;
|
|
@@ -180,17 +201,22 @@ function buildPredicateForField(field, attr, raw) {
|
|
|
180
201
|
return d ? { [field]: d } : null;
|
|
181
202
|
}
|
|
182
203
|
default:
|
|
183
|
-
return isArray
|
|
204
|
+
return isArray
|
|
205
|
+
? { [field]: { $in: tokens.length ? tokens : [raw] } }
|
|
206
|
+
: { [field]: raw };
|
|
184
207
|
}
|
|
185
208
|
}
|
|
186
209
|
function parseSort(expr) {
|
|
187
210
|
if (!expr)
|
|
188
211
|
return;
|
|
189
212
|
const out = {};
|
|
190
|
-
for (const raw of expr
|
|
191
|
-
|
|
213
|
+
for (const raw of expr
|
|
214
|
+
.split(',')
|
|
215
|
+
.map((s) => s.trim())
|
|
216
|
+
.filter(Boolean)) {
|
|
217
|
+
if (raw.startsWith('-'))
|
|
192
218
|
out[raw.slice(1)] = -1;
|
|
193
|
-
else if (raw.startsWith(
|
|
219
|
+
else if (raw.startsWith('+'))
|
|
194
220
|
out[raw.slice(1)] = 1;
|
|
195
221
|
else
|
|
196
222
|
out[raw] = 1;
|
|
@@ -198,15 +224,17 @@ function parseSort(expr) {
|
|
|
198
224
|
return Object.keys(out).length ? out : undefined;
|
|
199
225
|
}
|
|
200
226
|
function parseQuery(req) {
|
|
201
|
-
const limit = Math.min(parseInt(String(req.query.limit ??
|
|
202
|
-
const page = Math.max(parseInt(String(req.query.page ??
|
|
227
|
+
const limit = Math.min(parseInt(String(req.query.limit ?? '12'), 10) || 12, 100);
|
|
228
|
+
const page = Math.max(parseInt(String(req.query.page ?? '1'), 10) || 1, 1);
|
|
203
229
|
const skip = (page - 1) * limit;
|
|
204
|
-
const fields = String(req.query.fields ??
|
|
205
|
-
.split(
|
|
230
|
+
const fields = String(req.query.fields ?? '')
|
|
231
|
+
.split(',')
|
|
206
232
|
.map((s) => s.trim())
|
|
207
233
|
.filter(Boolean);
|
|
208
|
-
const projection = fields.length
|
|
209
|
-
|
|
234
|
+
const projection = fields.length
|
|
235
|
+
? Object.fromEntries(fields.map((f) => [f, 1]))
|
|
236
|
+
: undefined;
|
|
237
|
+
const sort = parseSort(String(req.query.sort ?? '-createdAt'));
|
|
210
238
|
return { limit, page, skip, projection, sort };
|
|
211
239
|
}
|
|
212
240
|
function extractIds(val) {
|
|
@@ -216,16 +244,16 @@ function extractIds(val) {
|
|
|
216
244
|
const toId = (v) => {
|
|
217
245
|
if (!v)
|
|
218
246
|
return null;
|
|
219
|
-
if (typeof v ===
|
|
247
|
+
if (typeof v === 'string')
|
|
220
248
|
return v;
|
|
221
|
-
if (typeof v ===
|
|
249
|
+
if (typeof v === 'number')
|
|
222
250
|
return String(v);
|
|
223
|
-
if (typeof v ===
|
|
224
|
-
if (typeof v.id ===
|
|
251
|
+
if (typeof v === 'object') {
|
|
252
|
+
if (typeof v.id === 'string')
|
|
225
253
|
return v.id;
|
|
226
|
-
if (v._id && typeof v._id ===
|
|
254
|
+
if (v._id && typeof v._id === 'string')
|
|
227
255
|
return v._id;
|
|
228
|
-
if (v._id && typeof v._id.toString ===
|
|
256
|
+
if (v._id && typeof v._id.toString === 'function')
|
|
229
257
|
return v._id.toString();
|
|
230
258
|
}
|
|
231
259
|
return null;
|
|
@@ -239,9 +267,16 @@ function refInfoFromAttr(attr) {
|
|
|
239
267
|
return { ref: String(attr[0].ref), isArray: true };
|
|
240
268
|
}
|
|
241
269
|
const a = Array.isArray(attr) ? attr?.[0] : attr;
|
|
242
|
-
const t = (typeof a?.type ===
|
|
243
|
-
if (a?.ref && (t ===
|
|
270
|
+
const t = (typeof a?.type === 'string' ? a.type : String(a?.type || '')).toLowerCase();
|
|
271
|
+
if (a?.ref && (t === 'objectid' || t === 'ref')) {
|
|
244
272
|
return { ref: String(a.ref), isArray: false };
|
|
245
273
|
}
|
|
246
274
|
return null;
|
|
247
275
|
}
|
|
276
|
+
const dedupeBy = (rows, key) => {
|
|
277
|
+
const map = new Map();
|
|
278
|
+
for (const r of rows)
|
|
279
|
+
map.set(key(r), r);
|
|
280
|
+
return Array.from(map.values());
|
|
281
|
+
};
|
|
282
|
+
exports.dedupeBy = dedupeBy;
|