@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.
@@ -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
+ }