@airoom/nextmin-node 0.1.4 → 0.1.6
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/dist/api/apiRouter.d.ts +6 -20
- package/dist/api/apiRouter.js +86 -1476
- package/dist/api/router/ctx.d.ts +25 -0
- package/dist/api/router/ctx.js +2 -0
- package/dist/api/router/mountCrudRoutes.d.ts +2 -0
- package/dist/api/router/mountCrudRoutes.js +754 -0
- package/dist/api/router/mountFindRoutes.d.ts +2 -0
- package/dist/api/router/mountFindRoutes.js +205 -0
- package/dist/api/router/setupAuthRoutes.d.ts +2 -0
- package/dist/api/router/setupAuthRoutes.js +247 -0
- package/dist/api/router/setupFileRoutes.d.ts +2 -0
- package/dist/api/router/setupFileRoutes.js +85 -0
- package/dist/api/router/utils.d.ts +63 -0
- package/dist/api/router/utils.js +247 -0
- package/dist/database/MongoAdapter.d.ts +1 -1
- package/dist/database/MongoAdapter.js +21 -32
- package/dist/schemas/Roles.json +7 -2
- package/dist/utils/DefaultDataInitializer.js +3 -0
- package/dist/utils/SchemaLoader.js +28 -7
- package/package.json +1 -1
|
@@ -0,0 +1,754 @@
|
|
|
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.mountCrudRoutes = mountCrudRoutes;
|
|
7
|
+
const bcrypt_1 = __importDefault(require("bcrypt"));
|
|
8
|
+
const Logger_1 = __importDefault(require("../../utils/Logger"));
|
|
9
|
+
const fieldCodecs_1 = require("../../utils/fieldCodecs");
|
|
10
|
+
const authorize_1 = require("../../policy/authorize");
|
|
11
|
+
const utils_1 = require("./utils");
|
|
12
|
+
function mountCrudRoutes(ctx, modelNameLC) {
|
|
13
|
+
const { router } = ctx;
|
|
14
|
+
const basePath = `/${modelNameLC}`;
|
|
15
|
+
const mwCreate = ctx.pickAuthFor('create', modelNameLC);
|
|
16
|
+
const mwRead = ctx.pickAuthFor('read', modelNameLC);
|
|
17
|
+
const mwUpdate = ctx.pickAuthFor('update', modelNameLC);
|
|
18
|
+
const mwDelete = ctx.pickAuthFor('delete', modelNameLC);
|
|
19
|
+
const ctxFromReq = (req) => {
|
|
20
|
+
const raw = req.user?.role;
|
|
21
|
+
let roleStr = null;
|
|
22
|
+
if (typeof raw === 'string') {
|
|
23
|
+
roleStr = raw;
|
|
24
|
+
}
|
|
25
|
+
else if (Array.isArray(raw)) {
|
|
26
|
+
const first = raw[0];
|
|
27
|
+
if (typeof first === 'string')
|
|
28
|
+
roleStr = first;
|
|
29
|
+
else if (first &&
|
|
30
|
+
typeof first === 'object' &&
|
|
31
|
+
typeof first.name === 'string') {
|
|
32
|
+
roleStr = first.name;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (raw &&
|
|
36
|
+
typeof raw === 'object' &&
|
|
37
|
+
typeof raw.name === 'string') {
|
|
38
|
+
roleStr = raw.name;
|
|
39
|
+
}
|
|
40
|
+
roleStr = roleStr ? roleStr.toLowerCase() : null;
|
|
41
|
+
return {
|
|
42
|
+
isAuthenticated: !!req.user,
|
|
43
|
+
role: roleStr,
|
|
44
|
+
userId: req.user?.id ?? req.user?._id ?? null,
|
|
45
|
+
isSuperadmin: roleStr === 'superadmin',
|
|
46
|
+
apiKeyOk: true,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
// ----------------- CREATE -----------------
|
|
50
|
+
router.post(basePath, mwCreate, async (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
const schema = ctx.getSchema(modelNameLC);
|
|
53
|
+
if (!schema.allowedMethods.create) {
|
|
54
|
+
return res
|
|
55
|
+
.status(405)
|
|
56
|
+
.json({ error: true, message: 'Method not allowed' });
|
|
57
|
+
}
|
|
58
|
+
const model = ctx.getModel(modelNameLC);
|
|
59
|
+
const pctx = ctxFromReq(req);
|
|
60
|
+
const schemaPolicy = {
|
|
61
|
+
allowedMethods: schema.allowedMethods,
|
|
62
|
+
access: schema.access,
|
|
63
|
+
};
|
|
64
|
+
const cdec = (0, authorize_1.authorize)(modelNameLC, 'create', schemaPolicy, pctx);
|
|
65
|
+
if (!cdec.allow) {
|
|
66
|
+
return res.status(403).json({ error: true, message: 'forbidden' });
|
|
67
|
+
}
|
|
68
|
+
let payload = (0, authorize_1.mergeCreateDefaults)(req.body, cdec.createDefaults);
|
|
69
|
+
(0, authorize_1.enforceRestrictions)(payload, cdec.restrictions, pctx);
|
|
70
|
+
if (!cdec.exposePrivate)
|
|
71
|
+
payload = (0, authorize_1.stripWriteDeny)(payload, cdec.writeDeny);
|
|
72
|
+
payload = (0, fieldCodecs_1.coerceForStorage)(schema, payload);
|
|
73
|
+
const missing = ctx.validateRequiredFields(schema, payload, 'create');
|
|
74
|
+
if (missing.length) {
|
|
75
|
+
return res.status(400).json({
|
|
76
|
+
error: true,
|
|
77
|
+
message: `Missing required fields: ${missing.join(', ')}`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (!schema.extends) {
|
|
81
|
+
const conflicts = await ctx.checkUniqueFields(schema, payload);
|
|
82
|
+
if (conflicts && conflicts.length > 0) {
|
|
83
|
+
return res.status(400).json({
|
|
84
|
+
error: true,
|
|
85
|
+
fields: conflicts.map((field) => ({
|
|
86
|
+
field,
|
|
87
|
+
error: true,
|
|
88
|
+
message: `You cannot use this ${field}. It's already been used.`,
|
|
89
|
+
})),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Extended schema: split base + child writes
|
|
94
|
+
if (schema.extends) {
|
|
95
|
+
const baseName = String(schema.extends);
|
|
96
|
+
const baseLC = baseName.toLowerCase();
|
|
97
|
+
const baseSchema = ctx.getSchema(baseLC);
|
|
98
|
+
const baseModel = ctx.getModel(baseLC);
|
|
99
|
+
const basePayload = {};
|
|
100
|
+
const childPayload = {};
|
|
101
|
+
for (const [k, v] of Object.entries(payload)) {
|
|
102
|
+
if (baseSchema.attributes[k])
|
|
103
|
+
basePayload[k] = v;
|
|
104
|
+
else if (k !== 'baseId')
|
|
105
|
+
childPayload[k] = v;
|
|
106
|
+
}
|
|
107
|
+
if (baseLC === 'users' && basePayload.password && ctx.jwtSecret) {
|
|
108
|
+
const salt = await bcrypt_1.default.genSalt(10);
|
|
109
|
+
basePayload.password = await bcrypt_1.default.hash(String(basePayload.password) + ctx.jwtSecret, salt);
|
|
110
|
+
}
|
|
111
|
+
const baseConflicts = await ctx.checkUniqueFields(baseSchema, basePayload);
|
|
112
|
+
if (baseConflicts && baseConflicts.length) {
|
|
113
|
+
return res.status(400).json({
|
|
114
|
+
error: true,
|
|
115
|
+
fields: baseConflicts.map((field) => ({
|
|
116
|
+
field,
|
|
117
|
+
error: true,
|
|
118
|
+
message: `You cannot use this ${field}. It's already been used.`,
|
|
119
|
+
})),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const childConflicts = await ctx.checkUniqueFields(schema, childPayload);
|
|
123
|
+
if (childConflicts && childConflicts.length) {
|
|
124
|
+
return res.status(400).json({
|
|
125
|
+
error: true,
|
|
126
|
+
fields: childConflicts.map((field) => ({
|
|
127
|
+
field,
|
|
128
|
+
error: true,
|
|
129
|
+
message: `You cannot use this ${field}. It's already been used.`,
|
|
130
|
+
})),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const baseCreated = await baseModel.create(basePayload);
|
|
134
|
+
const childToCreate = { ...childPayload, baseId: baseCreated.id };
|
|
135
|
+
const childCreated = await model.create(childToCreate);
|
|
136
|
+
let resultDoc = { ...baseCreated, ...childCreated };
|
|
137
|
+
delete resultDoc.baseId;
|
|
138
|
+
if (cdec.exposePrivate && childCreated?.id) {
|
|
139
|
+
const [refChild] = await model.read({ id: childCreated.id }, 1, 0, true);
|
|
140
|
+
const [refBase] = await baseModel.read({ id: baseCreated.id }, 1, 0, true);
|
|
141
|
+
if (refChild && refBase) {
|
|
142
|
+
resultDoc = { ...refBase, ...refChild };
|
|
143
|
+
delete resultDoc.baseId;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const masked = cdec.exposePrivate
|
|
147
|
+
? (0, authorize_1.applyReadMaskOne)(resultDoc, cdec.sensitiveMask)
|
|
148
|
+
: (0, authorize_1.applyReadMaskOne)(resultDoc, cdec.readMask);
|
|
149
|
+
return res.status(201).json({
|
|
150
|
+
success: true,
|
|
151
|
+
message: `${schema.modelName} has been created successfully.`,
|
|
152
|
+
data: masked,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
// non-extended
|
|
156
|
+
if (modelNameLC === 'users' &&
|
|
157
|
+
payload.password &&
|
|
158
|
+
ctx.jwtSecret) {
|
|
159
|
+
const salt = await bcrypt_1.default.genSalt(10);
|
|
160
|
+
payload.password = await bcrypt_1.default.hash(String(payload.password) + ctx.jwtSecret, salt);
|
|
161
|
+
}
|
|
162
|
+
const created = await model.create(payload);
|
|
163
|
+
let resultDoc = created;
|
|
164
|
+
if (cdec.exposePrivate && created?.id) {
|
|
165
|
+
const refetched = await model.read({ id: created.id }, 1, 0, true);
|
|
166
|
+
if (refetched?.[0])
|
|
167
|
+
resultDoc = refetched[0];
|
|
168
|
+
}
|
|
169
|
+
const masked = cdec.exposePrivate
|
|
170
|
+
? (0, authorize_1.applyReadMaskOne)(resultDoc, cdec.sensitiveMask)
|
|
171
|
+
: (0, authorize_1.applyReadMaskOne)(resultDoc, cdec.readMask);
|
|
172
|
+
return res.status(201).json({
|
|
173
|
+
success: true,
|
|
174
|
+
message: `${schema.modelName} has been created successfully.`,
|
|
175
|
+
data: masked,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (error?.code === 'MODEL_REMOVED') {
|
|
180
|
+
return res.status(410).json({ error: true, message: error.message });
|
|
181
|
+
}
|
|
182
|
+
ctx.handleWriteError(error, res);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
// ----------------- READ (list) -----------------
|
|
186
|
+
router.get(basePath, mwRead, async (req, res) => {
|
|
187
|
+
try {
|
|
188
|
+
const schema = ctx.getSchema(modelNameLC);
|
|
189
|
+
if (!schema.allowedMethods.read) {
|
|
190
|
+
return res
|
|
191
|
+
.status(405)
|
|
192
|
+
.json({ error: true, message: 'Method not allowed' });
|
|
193
|
+
}
|
|
194
|
+
const model = ctx.getModel(modelNameLC);
|
|
195
|
+
const page = Number.parseInt(String(req.query.page ?? '0'), 10) || 0;
|
|
196
|
+
const limit = Number.parseInt(String(req.query.limit ?? '10'), 10) || 10;
|
|
197
|
+
const q = String(req.query.q ?? '').trim();
|
|
198
|
+
const searchKey = String(req.query.searchKey ?? '').trim();
|
|
199
|
+
const searchKeysCSV = String(req.query.searchKeys ?? '').trim();
|
|
200
|
+
const searchMode = /^(and|or)$/i.test(String(req.query.searchMode ?? ''))
|
|
201
|
+
? String(req.query.searchMode).toLowerCase()
|
|
202
|
+
: 'or';
|
|
203
|
+
const dateFromStr = String(req.query.dateFrom ?? '').trim();
|
|
204
|
+
const dateToStr = String(req.query.dateTo ?? '').trim();
|
|
205
|
+
let dateKey = String(req.query.dateKey ?? 'createdAt').trim();
|
|
206
|
+
const sortRaw = String(req.query.sort ?? '').trim();
|
|
207
|
+
const sortTypeRaw = String(req.query.sortType ?? '').trim();
|
|
208
|
+
const filter = {};
|
|
209
|
+
const buildSortSpec = () => {
|
|
210
|
+
const keys = (0, utils_1.splitCSV)(sortRaw);
|
|
211
|
+
const dirs = (0, utils_1.splitCSV)(sortTypeRaw);
|
|
212
|
+
const s = {};
|
|
213
|
+
keys.forEach((k, i) => {
|
|
214
|
+
if (!schema.attributes?.[k])
|
|
215
|
+
return;
|
|
216
|
+
const d = dirs[i] ?? dirs[dirs.length - 1] ?? 'desc';
|
|
217
|
+
s[k] = /^(desc|-1)$/i.test(d) ? -1 : 1;
|
|
218
|
+
});
|
|
219
|
+
if (!Object.keys(s).length)
|
|
220
|
+
s.createdAt = -1;
|
|
221
|
+
return s;
|
|
222
|
+
};
|
|
223
|
+
// SEARCH
|
|
224
|
+
const ors = [];
|
|
225
|
+
const ands = [];
|
|
226
|
+
if (q && searchKey && schema.attributes?.[searchKey]) {
|
|
227
|
+
const p = (0, utils_1.buildPredicateForField)(searchKey, schema.attributes[searchKey], q);
|
|
228
|
+
if (p)
|
|
229
|
+
ors.push(p);
|
|
230
|
+
}
|
|
231
|
+
if (q && searchKeysCSV) {
|
|
232
|
+
const keys = (0, utils_1.splitCSV)(searchKeysCSV);
|
|
233
|
+
const preds = keys
|
|
234
|
+
.filter((k) => !!schema.attributes?.[k])
|
|
235
|
+
.map((k) => (0, utils_1.buildPredicateForField)(k, schema.attributes[k], q))
|
|
236
|
+
.filter(Boolean);
|
|
237
|
+
if (preds.length) {
|
|
238
|
+
if (searchMode === 'and')
|
|
239
|
+
ands.push(...preds);
|
|
240
|
+
else
|
|
241
|
+
ors.push(...preds);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (ands.length && ors.length)
|
|
245
|
+
filter.$and = [...ands, { $or: ors }];
|
|
246
|
+
else if (ands.length)
|
|
247
|
+
filter.$and = [...(filter.$and || []), ...ands];
|
|
248
|
+
else if (ors.length)
|
|
249
|
+
filter.$or = [...(filter.$or || []), ...ors];
|
|
250
|
+
if (dateFromStr || dateToStr) {
|
|
251
|
+
if (!schema.attributes?.[dateKey]) {
|
|
252
|
+
if (schema.attributes?.createdAt)
|
|
253
|
+
dateKey = 'createdAt';
|
|
254
|
+
else if (schema.attributes?.updatedAt)
|
|
255
|
+
dateKey = 'updatedAt';
|
|
256
|
+
}
|
|
257
|
+
if (dateKey && schema.attributes?.[dateKey]) {
|
|
258
|
+
const range = {};
|
|
259
|
+
if (dateFromStr) {
|
|
260
|
+
const d = new Date(dateFromStr);
|
|
261
|
+
if (!Number.isNaN(+d))
|
|
262
|
+
range.$gte = d;
|
|
263
|
+
}
|
|
264
|
+
if (dateToStr) {
|
|
265
|
+
const d = new Date(dateToStr);
|
|
266
|
+
if (!Number.isNaN(+d))
|
|
267
|
+
range.$lte = d;
|
|
268
|
+
}
|
|
269
|
+
if (Object.keys(range).length)
|
|
270
|
+
filter[dateKey] = range;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// arbitrary field filters ---- Direct field filters from req.query (e.g., ?isPublic=true&key=doctor) ----
|
|
274
|
+
const RESERVED = new Set([
|
|
275
|
+
'page',
|
|
276
|
+
'limit',
|
|
277
|
+
'q',
|
|
278
|
+
'searchKey',
|
|
279
|
+
'searchKeys',
|
|
280
|
+
'searchMode',
|
|
281
|
+
'dateFrom',
|
|
282
|
+
'dateTo',
|
|
283
|
+
'dateKey',
|
|
284
|
+
'sort',
|
|
285
|
+
'sortType',
|
|
286
|
+
]);
|
|
287
|
+
const attrMap = schema.attributes || {};
|
|
288
|
+
const coerceQueryVal = (raw) => {
|
|
289
|
+
const s = String(raw).trim();
|
|
290
|
+
if (/^(true|false)$/i.test(s))
|
|
291
|
+
return /^true$/i.test(s);
|
|
292
|
+
if (/^-?\d+(\.\d+)?$/.test(s))
|
|
293
|
+
return Number(s);
|
|
294
|
+
if (/^null$/i.test(s))
|
|
295
|
+
return null;
|
|
296
|
+
if (/^undefined$/i.test(s))
|
|
297
|
+
return undefined;
|
|
298
|
+
return s;
|
|
299
|
+
};
|
|
300
|
+
for (const [k, vRaw] of Object.entries(req.query)) {
|
|
301
|
+
if (RESERVED.has(k))
|
|
302
|
+
continue;
|
|
303
|
+
if (!Object.prototype.hasOwnProperty.call(attrMap, k))
|
|
304
|
+
continue;
|
|
305
|
+
const v = Array.isArray(vRaw)
|
|
306
|
+
? vRaw.map(String).join(',')
|
|
307
|
+
: String(vRaw);
|
|
308
|
+
if (!v?.length)
|
|
309
|
+
continue;
|
|
310
|
+
// Operators:
|
|
311
|
+
// - "a,b,c" -> $in
|
|
312
|
+
// - "!x" -> $ne
|
|
313
|
+
// - "gt:10" -> {$gt:10}, "gte:10","lt:5","lte:5"
|
|
314
|
+
// - default -> exact match
|
|
315
|
+
const tryNumOrBool = (s) => coerceQueryVal(s);
|
|
316
|
+
if (v.includes(',')) {
|
|
317
|
+
const arr = v
|
|
318
|
+
.split(',')
|
|
319
|
+
.map((s) => s.trim())
|
|
320
|
+
.filter(Boolean)
|
|
321
|
+
.map(tryNumOrBool);
|
|
322
|
+
filter[k] = { $in: arr };
|
|
323
|
+
}
|
|
324
|
+
else if (v.startsWith('!')) {
|
|
325
|
+
filter[k] = { $ne: tryNumOrBool(v.slice(1)) };
|
|
326
|
+
}
|
|
327
|
+
else if (/^(gt|gte|lt|lte):/i.test(v)) {
|
|
328
|
+
const [op, rest] = v.split(':', 2);
|
|
329
|
+
const map = {
|
|
330
|
+
gt: '$gt',
|
|
331
|
+
gte: '$gte',
|
|
332
|
+
lt: '$lt',
|
|
333
|
+
lte: '$lte',
|
|
334
|
+
};
|
|
335
|
+
filter[k] = { [map[op.toLowerCase()]]: tryNumOrBool(rest) };
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
filter[k] = tryNumOrBool(v);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const pctx = ctxFromReq(req);
|
|
342
|
+
const schemaPolicy = {
|
|
343
|
+
allowedMethods: schema.allowedMethods,
|
|
344
|
+
access: schema.access,
|
|
345
|
+
};
|
|
346
|
+
const rdec = (0, authorize_1.authorize)(modelNameLC, 'read', schemaPolicy, pctx);
|
|
347
|
+
if (!rdec.allow) {
|
|
348
|
+
return res.status(403).json({ error: true, message: 'forbidden' });
|
|
349
|
+
}
|
|
350
|
+
const baseSort = buildSortSpec();
|
|
351
|
+
const finalFilter = (0, authorize_1.andFilter)(filter, rdec.queryFilter || {});
|
|
352
|
+
const isUsersModel = modelNameLC === 'user' ||
|
|
353
|
+
modelNameLC === 'users' ||
|
|
354
|
+
(schema?.modelName ?? '').toLowerCase() === 'users';
|
|
355
|
+
const currentUserId = req?.user?.id;
|
|
356
|
+
// EXTENDED PATH
|
|
357
|
+
if (schema.extends) {
|
|
358
|
+
const baseName = String(schema.extends);
|
|
359
|
+
const baseLC = baseName.toLowerCase();
|
|
360
|
+
const baseSchema = ctx.getSchema(baseLC);
|
|
361
|
+
const baseModel = ctx.getModel(baseLC);
|
|
362
|
+
const baseKeys = new Set(Object.keys(baseSchema.attributes || {}));
|
|
363
|
+
const { child: childFilter, base: extBaseFilter } = (0, utils_1.splitFilterForExtended)(finalFilter, baseKeys);
|
|
364
|
+
const { child: childSort, base: extBaseSort } = (0, utils_1.splitSortForExtended)(baseSort, baseKeys);
|
|
365
|
+
const needPrivateForHydrate = true;
|
|
366
|
+
const requiresBaseProcessing = Object.keys(extBaseFilter).length > 0 ||
|
|
367
|
+
Object.keys(extBaseSort).length > 0;
|
|
368
|
+
let childRows = [];
|
|
369
|
+
if (requiresBaseProcessing) {
|
|
370
|
+
childRows = await model.read(childFilter, 0, 0, needPrivateForHydrate);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
childRows = await model.read(childFilter, limit, page * limit, needPrivateForHydrate, {
|
|
374
|
+
sort: Object.keys(childSort).length ? childSort : undefined,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
const baseIds = Array.from(new Set(childRows
|
|
378
|
+
.map((r) => (0, utils_1.toIdString)(r?.baseId))
|
|
379
|
+
.filter((s) => !!s)));
|
|
380
|
+
const baseDocs = baseIds.length
|
|
381
|
+
? await baseModel.read({ id: { $in: baseIds } }, baseIds.length, 0, !!rdec.exposePrivate)
|
|
382
|
+
: [];
|
|
383
|
+
const baseMap = new Map(baseDocs.map((b) => [String(b.id), b]));
|
|
384
|
+
let merged = childRows.map((row) => {
|
|
385
|
+
const bid = (0, utils_1.toIdString)(row?.baseId);
|
|
386
|
+
const b = bid ? baseMap.get(bid) : null;
|
|
387
|
+
const m = b ? { ...b, ...row } : { ...row };
|
|
388
|
+
delete m.baseId;
|
|
389
|
+
return m;
|
|
390
|
+
});
|
|
391
|
+
if (Object.keys(extBaseFilter).length) {
|
|
392
|
+
merged = merged.filter((m) => (0, utils_1.matchDoc)(m, extBaseFilter));
|
|
393
|
+
}
|
|
394
|
+
const combinedSort = { ...extBaseSort, ...childSort };
|
|
395
|
+
if (Object.keys(combinedSort).length) {
|
|
396
|
+
merged = (0, utils_1.sortInMemory)(merged, combinedSort);
|
|
397
|
+
}
|
|
398
|
+
if (currentUserId && isUsersModel) {
|
|
399
|
+
merged = merged.filter((r) => String(r?.id) !== String(currentUserId));
|
|
400
|
+
}
|
|
401
|
+
let totalRows;
|
|
402
|
+
let paged;
|
|
403
|
+
if (requiresBaseProcessing) {
|
|
404
|
+
totalRows = merged.length;
|
|
405
|
+
const start = page * limit;
|
|
406
|
+
paged = merged.slice(start, start + limit);
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
totalRows = await model.count(childFilter);
|
|
410
|
+
paged = merged.slice(0, limit);
|
|
411
|
+
}
|
|
412
|
+
const data = rdec.exposePrivate
|
|
413
|
+
? (0, authorize_1.applyReadMaskMany)(paged, rdec.sensitiveMask)
|
|
414
|
+
: (0, authorize_1.applyReadMaskMany)(paged, rdec.readMask);
|
|
415
|
+
return res.status(200).json({
|
|
416
|
+
success: true,
|
|
417
|
+
message: `Data fetched for ${schema.modelName}`,
|
|
418
|
+
data,
|
|
419
|
+
pagination: { totalRows, page, limit },
|
|
420
|
+
sort: combinedSort,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
// NON-EXTENDED
|
|
424
|
+
const sort = baseSort;
|
|
425
|
+
const totalRows = await model.count(finalFilter);
|
|
426
|
+
let rawRows = [];
|
|
427
|
+
const exposePrivateForRead = !!rdec.exposePrivate;
|
|
428
|
+
try {
|
|
429
|
+
rawRows = await model.read(finalFilter, limit + 1, page * limit, exposePrivateForRead, { sort });
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
rawRows = await model.read(finalFilter, limit + 1, page * limit, exposePrivateForRead);
|
|
433
|
+
if (sort && Object.keys(sort).length) {
|
|
434
|
+
const orderKeys = Object.keys(sort);
|
|
435
|
+
rawRows.sort((a, b) => {
|
|
436
|
+
for (const k of orderKeys) {
|
|
437
|
+
const dir = sort[k];
|
|
438
|
+
const av = a?.[k];
|
|
439
|
+
const bv = b?.[k];
|
|
440
|
+
const ax = av instanceof Date ? +av : (av?.toString?.() ?? av);
|
|
441
|
+
const bx = bv instanceof Date ? +bv : (bv?.toString?.() ?? bv);
|
|
442
|
+
if (ax == null && bx == null)
|
|
443
|
+
continue;
|
|
444
|
+
if (ax == null)
|
|
445
|
+
return 1 * dir;
|
|
446
|
+
if (bx == null)
|
|
447
|
+
return -1 * dir;
|
|
448
|
+
if (ax > bx)
|
|
449
|
+
return 1 * dir;
|
|
450
|
+
if (ax < bx)
|
|
451
|
+
return -1 * dir;
|
|
452
|
+
}
|
|
453
|
+
return 0;
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
let rows = currentUserId && isUsersModel
|
|
458
|
+
? rawRows
|
|
459
|
+
.filter((r) => String(r?.id) !== String(currentUserId))
|
|
460
|
+
.slice(0, limit)
|
|
461
|
+
: rawRows.slice(0, limit);
|
|
462
|
+
const data = rdec.exposePrivate
|
|
463
|
+
? (0, authorize_1.applyReadMaskMany)(rows, rdec.sensitiveMask)
|
|
464
|
+
: (0, authorize_1.applyReadMaskMany)(rows, rdec.readMask);
|
|
465
|
+
return res.status(200).json({
|
|
466
|
+
success: true,
|
|
467
|
+
message: `Data fetched for ${schema.modelName}`,
|
|
468
|
+
data,
|
|
469
|
+
pagination: { totalRows, page, limit },
|
|
470
|
+
sort,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
if (error?.code === 'MODEL_REMOVED') {
|
|
475
|
+
return res.status(410).json({ error: true, message: error.message });
|
|
476
|
+
}
|
|
477
|
+
return res.status(400).json({ error: true, message: error.message });
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
// ----------------- READ (by id) -----------------
|
|
481
|
+
router.get(`${basePath}/:id`, mwRead, async (req, res) => {
|
|
482
|
+
try {
|
|
483
|
+
const schema = ctx.getSchema(modelNameLC);
|
|
484
|
+
if (!schema.allowedMethods.read) {
|
|
485
|
+
return res
|
|
486
|
+
.status(405)
|
|
487
|
+
.json({ error: true, message: 'Method not allowed' });
|
|
488
|
+
}
|
|
489
|
+
const model = ctx.getModel(modelNameLC);
|
|
490
|
+
const pctx = ctxFromReq(req);
|
|
491
|
+
const schemaPolicy = {
|
|
492
|
+
allowedMethods: schema.allowedMethods,
|
|
493
|
+
access: schema.access,
|
|
494
|
+
};
|
|
495
|
+
const decForInclude = (0, authorize_1.authorize)(modelNameLC, 'read', schemaPolicy, pctx);
|
|
496
|
+
const needPrivateForHydrate = !!schema.extends;
|
|
497
|
+
const exposePrivateForRead = needPrivateForHydrate || !!decForInclude.exposePrivate;
|
|
498
|
+
const recordArr = await model.read({ id: req.params.id }, 1, 0, exposePrivateForRead);
|
|
499
|
+
const doc = recordArr?.[0];
|
|
500
|
+
if (!doc) {
|
|
501
|
+
return res
|
|
502
|
+
.status(404)
|
|
503
|
+
.json({ error: true, message: `${schema.modelName} not found` });
|
|
504
|
+
}
|
|
505
|
+
const dec = (0, authorize_1.authorize)(modelNameLC, 'read', schemaPolicy, pctx, doc);
|
|
506
|
+
if (!dec.allow)
|
|
507
|
+
return res.status(403).json({ error: true, message: 'forbidden' });
|
|
508
|
+
let toReturn = doc;
|
|
509
|
+
if (schema.extends) {
|
|
510
|
+
const baseName = String(schema.extends);
|
|
511
|
+
const baseLC = baseName.toLowerCase();
|
|
512
|
+
const baseModel = ctx.getModel(baseLC);
|
|
513
|
+
const baseIdStr = (0, utils_1.toIdString)(doc?.baseId);
|
|
514
|
+
if (baseIdStr) {
|
|
515
|
+
const [baseDoc] = await baseModel.read({ id: baseIdStr }, 1, 0, !!dec.exposePrivate);
|
|
516
|
+
if (baseDoc) {
|
|
517
|
+
toReturn = { ...baseDoc, ...doc };
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (toReturn)
|
|
521
|
+
delete toReturn.baseId;
|
|
522
|
+
}
|
|
523
|
+
const data = dec.exposePrivate
|
|
524
|
+
? (0, authorize_1.applyReadMaskOne)(toReturn, dec.sensitiveMask)
|
|
525
|
+
: (0, authorize_1.applyReadMaskOne)(toReturn, dec.readMask);
|
|
526
|
+
return res.status(200).json({
|
|
527
|
+
success: true,
|
|
528
|
+
message: `${schema.modelName} found`,
|
|
529
|
+
data,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
if (error?.code === 'MODEL_REMOVED')
|
|
534
|
+
return res.status(410).json({ error: true, message: error.message });
|
|
535
|
+
Logger_1.default.error('error', error.message);
|
|
536
|
+
res.status(400).json({ error: true, message: error.message });
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
// ----------------- UPDATE -----------------
|
|
540
|
+
router.put(`${basePath}/:id`, mwUpdate, async (req, res) => {
|
|
541
|
+
try {
|
|
542
|
+
const schema = ctx.getSchema(modelNameLC);
|
|
543
|
+
if (!schema.allowedMethods.update) {
|
|
544
|
+
return res
|
|
545
|
+
.status(405)
|
|
546
|
+
.json({ error: true, message: 'Method not allowed' });
|
|
547
|
+
}
|
|
548
|
+
const model = ctx.getModel(modelNameLC);
|
|
549
|
+
const beforeArr = await model.read({ id: req.params.id }, 1, 0, true);
|
|
550
|
+
const before = beforeArr?.[0];
|
|
551
|
+
const pctx = ctxFromReq(req);
|
|
552
|
+
const schemaPolicy = {
|
|
553
|
+
allowedMethods: schema.allowedMethods,
|
|
554
|
+
access: schema.access,
|
|
555
|
+
};
|
|
556
|
+
const udec = (0, authorize_1.authorize)(modelNameLC, 'update', schemaPolicy, pctx, before);
|
|
557
|
+
if (!udec.allow) {
|
|
558
|
+
return res.status(403).json({ error: true, message: 'forbidden' });
|
|
559
|
+
}
|
|
560
|
+
let upd = { ...req.body };
|
|
561
|
+
(0, authorize_1.enforceRestrictions)(upd, udec.restrictions, pctx);
|
|
562
|
+
if (!udec.exposePrivate)
|
|
563
|
+
upd = (0, authorize_1.stripWriteDeny)(upd, udec.writeDeny);
|
|
564
|
+
upd = (0, fieldCodecs_1.coerceForStorage)(schema, upd);
|
|
565
|
+
if (modelNameLC === 'users' &&
|
|
566
|
+
Object.prototype.hasOwnProperty.call(upd, 'password')) {
|
|
567
|
+
if (!upd.password) {
|
|
568
|
+
delete upd.password;
|
|
569
|
+
}
|
|
570
|
+
else if (ctx.jwtSecret) {
|
|
571
|
+
const salt = await bcrypt_1.default.genSalt(10);
|
|
572
|
+
upd.password = await bcrypt_1.default.hash(String(upd.password) + ctx.jwtSecret, salt);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const missing = ctx.validateRequiredFields(schema, upd, 'update');
|
|
576
|
+
if (missing.length) {
|
|
577
|
+
return res.status(400).json({
|
|
578
|
+
error: true,
|
|
579
|
+
message: `Missing required fields: ${missing.join(', ')}`,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
// Extended
|
|
583
|
+
if (schema.extends) {
|
|
584
|
+
const baseName = String(schema.extends);
|
|
585
|
+
const baseLC = baseName.toLowerCase();
|
|
586
|
+
const baseSchema = ctx.getSchema(baseLC);
|
|
587
|
+
const baseModel = ctx.getModel(baseLC);
|
|
588
|
+
const baseIdRaw = before?.baseId;
|
|
589
|
+
const baseId = (0, utils_1.toIdString)(baseIdRaw);
|
|
590
|
+
if (!baseId) {
|
|
591
|
+
return res.status(400).json({
|
|
592
|
+
error: true,
|
|
593
|
+
message: 'Invalid extended record: missing baseId',
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
const baseUpd = {};
|
|
597
|
+
const childUpd = {};
|
|
598
|
+
for (const [k, v] of Object.entries(upd)) {
|
|
599
|
+
if (k === 'baseId')
|
|
600
|
+
continue;
|
|
601
|
+
if (baseSchema.attributes[k])
|
|
602
|
+
baseUpd[k] = v;
|
|
603
|
+
else
|
|
604
|
+
childUpd[k] = v;
|
|
605
|
+
}
|
|
606
|
+
const baseConflicts = await ctx.checkUniqueFields(baseSchema, baseUpd, baseId);
|
|
607
|
+
if (baseConflicts && baseConflicts.length) {
|
|
608
|
+
return res.status(400).json({
|
|
609
|
+
error: true,
|
|
610
|
+
message: 'There are some error while updating record.',
|
|
611
|
+
fields: baseConflicts.map((field) => ({
|
|
612
|
+
field,
|
|
613
|
+
error: true,
|
|
614
|
+
message: `You cannot use this ${field}. It's already been used.`,
|
|
615
|
+
})),
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
const childConflicts = await ctx.checkUniqueFields(schema, childUpd, req.params.id);
|
|
619
|
+
if (childConflicts && childConflicts.length) {
|
|
620
|
+
return res.status(400).json({
|
|
621
|
+
error: true,
|
|
622
|
+
message: 'There are some error while updating record.',
|
|
623
|
+
fields: childConflicts.map((field) => ({
|
|
624
|
+
field,
|
|
625
|
+
error: true,
|
|
626
|
+
message: `You cannot use this ${field}. It's already been used.`,
|
|
627
|
+
})),
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
if (baseLC === 'users' &&
|
|
631
|
+
Object.prototype.hasOwnProperty.call(baseUpd, 'password')) {
|
|
632
|
+
if (!baseUpd.password) {
|
|
633
|
+
delete baseUpd.password;
|
|
634
|
+
}
|
|
635
|
+
else if (ctx.jwtSecret) {
|
|
636
|
+
const salt = await bcrypt_1.default.genSalt(10);
|
|
637
|
+
baseUpd.password = await bcrypt_1.default.hash(String(baseUpd.password) + ctx.jwtSecret, salt);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
let updatedChild = before;
|
|
641
|
+
if (Object.keys(childUpd).length) {
|
|
642
|
+
updatedChild = await model.update(req.params.id, childUpd);
|
|
643
|
+
}
|
|
644
|
+
if (Object.keys(baseUpd).length) {
|
|
645
|
+
await baseModel.update(baseId, baseUpd);
|
|
646
|
+
}
|
|
647
|
+
const [refChild] = await model.read({ id: String(updatedChild.id) }, 1, 0, true);
|
|
648
|
+
const [refBase] = await baseModel.read({ id: baseId }, 1, 0, true);
|
|
649
|
+
let responseDoc = refChild && refBase ? { ...refBase, ...refChild } : updatedChild;
|
|
650
|
+
if (responseDoc)
|
|
651
|
+
delete responseDoc.baseId;
|
|
652
|
+
const masked = udec.exposePrivate
|
|
653
|
+
? (0, authorize_1.applyReadMaskOne)(responseDoc, udec.sensitiveMask)
|
|
654
|
+
: (0, authorize_1.applyReadMaskOne)(responseDoc, udec.readMask);
|
|
655
|
+
return res.json(masked);
|
|
656
|
+
}
|
|
657
|
+
// non-extended
|
|
658
|
+
const conflicts = await ctx.checkUniqueFields(schema, upd, req.params.id);
|
|
659
|
+
if (conflicts && conflicts.length) {
|
|
660
|
+
return res.status(400).json({
|
|
661
|
+
error: true,
|
|
662
|
+
message: 'There are some error while updating record.',
|
|
663
|
+
fields: conflicts.map((field) => ({
|
|
664
|
+
field,
|
|
665
|
+
error: true,
|
|
666
|
+
message: `You cannot use this ${field}. It's already been used.`,
|
|
667
|
+
})),
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
const updatedRecord = await model.update(req.params.id, upd);
|
|
671
|
+
let responseDoc = updatedRecord;
|
|
672
|
+
if (udec.exposePrivate && updatedRecord?.id) {
|
|
673
|
+
const refetched = await model.read({ id: updatedRecord.id }, 1, 0, true);
|
|
674
|
+
if (refetched?.[0])
|
|
675
|
+
responseDoc = refetched[0];
|
|
676
|
+
}
|
|
677
|
+
const masked = udec.exposePrivate
|
|
678
|
+
? (0, authorize_1.applyReadMaskOne)(responseDoc, udec.sensitiveMask)
|
|
679
|
+
: (0, authorize_1.applyReadMaskOne)(responseDoc, udec.readMask);
|
|
680
|
+
return res.json(masked);
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
if (error?.code === 'MODEL_REMOVED') {
|
|
684
|
+
return res.status(410).json({ error: true, message: error.message });
|
|
685
|
+
}
|
|
686
|
+
ctx.handleWriteError(error, res);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
// ----------------- DELETE -----------------
|
|
690
|
+
router.delete(`${basePath}/:id`, mwDelete, async (req, res) => {
|
|
691
|
+
try {
|
|
692
|
+
const schema = ctx.getSchema(modelNameLC);
|
|
693
|
+
if (!schema.allowedMethods.delete) {
|
|
694
|
+
return res
|
|
695
|
+
.status(405)
|
|
696
|
+
.json({ error: true, message: 'Method not allowed' });
|
|
697
|
+
}
|
|
698
|
+
const model = ctx.getModel(modelNameLC);
|
|
699
|
+
const docArr = await model.read({ id: req.params.id }, 1, 0, true);
|
|
700
|
+
const doc = docArr?.[0];
|
|
701
|
+
const pctx = ctxFromReq(req);
|
|
702
|
+
const schemaPolicy = {
|
|
703
|
+
allowedMethods: schema.allowedMethods,
|
|
704
|
+
access: schema.access,
|
|
705
|
+
};
|
|
706
|
+
const ddec = (0, authorize_1.authorize)(modelNameLC, 'delete', schemaPolicy, pctx, doc);
|
|
707
|
+
if (!ddec.allow)
|
|
708
|
+
return res.status(403).json({ error: true, message: 'forbidden' });
|
|
709
|
+
if (schema.extends) {
|
|
710
|
+
const baseName = String(schema.extends);
|
|
711
|
+
const baseLC = baseName.toLowerCase();
|
|
712
|
+
const baseModel = ctx.getModel(baseLC);
|
|
713
|
+
const baseId = doc?.baseId;
|
|
714
|
+
const deletedChild = await model.delete(req.params.id);
|
|
715
|
+
if (baseId) {
|
|
716
|
+
try {
|
|
717
|
+
await baseModel.delete(String(baseId));
|
|
718
|
+
}
|
|
719
|
+
catch { }
|
|
720
|
+
}
|
|
721
|
+
const merged = baseId
|
|
722
|
+
? { ...deletedChild, baseId: undefined }
|
|
723
|
+
: deletedChild;
|
|
724
|
+
const masked = ddec.exposePrivate
|
|
725
|
+
? (0, authorize_1.applyReadMaskOne)(merged, ddec.sensitiveMask)
|
|
726
|
+
: (0, authorize_1.applyReadMaskOne)(merged, ddec.readMask);
|
|
727
|
+
return res.json({
|
|
728
|
+
success: true,
|
|
729
|
+
message: `We have deleted the record successfully.`,
|
|
730
|
+
data: masked,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
const deletedRecord = await model.delete(req.params.id);
|
|
734
|
+
if (!deletedRecord) {
|
|
735
|
+
return res
|
|
736
|
+
.status(404)
|
|
737
|
+
.json({ error: true, message: `${schema.modelName} not found` });
|
|
738
|
+
}
|
|
739
|
+
const masked = ddec.exposePrivate
|
|
740
|
+
? (0, authorize_1.applyReadMaskOne)(deletedRecord, ddec.sensitiveMask)
|
|
741
|
+
: (0, authorize_1.applyReadMaskOne)(deletedRecord, ddec.readMask);
|
|
742
|
+
return res.json({
|
|
743
|
+
success: true,
|
|
744
|
+
message: `We have deleted the record successfully.`,
|
|
745
|
+
data: masked,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
if (error?.code === 'MODEL_REMOVED')
|
|
750
|
+
return res.status(410).json({ error: true, message: error.message });
|
|
751
|
+
res.status(400).json({ error: true, message: error.message });
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
}
|