@airoom/nextmin-node 0.1.4 → 0.1.5

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.
@@ -7,16 +7,14 @@ exports.APIRouter = void 0;
7
7
  const express_1 = __importDefault(require("express"));
8
8
  const BaseModel_1 = require("../models/BaseModel");
9
9
  const Logger_1 = __importDefault(require("../utils/Logger"));
10
- const bcrypt_1 = __importDefault(require("bcrypt"));
11
10
  const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
12
11
  const SchemaLoader_1 = require("../utils/SchemaLoader");
13
12
  const InMemoryAdapter_1 = require("../database/InMemoryAdapter");
14
13
  const DefaultDataInitializer_1 = require("../utils/DefaultDataInitializer");
15
14
  const SchemaService_1 = require("../services/SchemaService");
16
- const multer_1 = __importDefault(require("multer"));
17
- const filename_1 = require("../files/filename");
18
- const authorize_1 = require("../policy/authorize");
19
- const fieldCodecs_1 = require("../utils/fieldCodecs");
15
+ const setupAuthRoutes_1 = require("./router/setupAuthRoutes");
16
+ const mountCrudRoutes_1 = require("./router/mountCrudRoutes");
17
+ const mountFindRoutes_1 = require("./router/mountFindRoutes");
20
18
  class APIRouter {
21
19
  constructor(options) {
22
20
  this.models = {};
@@ -25,12 +23,12 @@ class APIRouter {
25
23
  this.schemasRouteRegistered = false;
26
24
  this.registeredModels = new Set();
27
25
  this.liveSchemas = {};
28
- // ---------------- Live lookups ----------------
26
+ // ---------- Live lookups ----------
29
27
  this.getSchema = (name) => {
30
28
  const s = this.liveSchemas[name];
31
29
  if (!s) {
32
30
  const err = new Error(`Model '${name}' removed`);
33
- err.code = 'MODEL_REMOVED';
31
+ err.code = "MODEL_REMOVED";
34
32
  throw err;
35
33
  }
36
34
  return s;
@@ -40,19 +38,18 @@ class APIRouter {
40
38
  const m = this.models[name];
41
39
  if (!m) {
42
40
  const err = new Error(`Model '${name}' unavailable`);
43
- err.code = 'MODEL_UNAVAILABLE';
41
+ err.code = "MODEL_UNAVAILABLE";
44
42
  throw err;
45
43
  }
46
44
  return m;
47
45
  };
48
- this.fileAuthMiddleware = (req, res, next) => this.authenticateMiddleware(req, res, next);
49
- // ---------------- Auth (login/me/change-password) ----------------
46
+ // ---------- middleware ----------
50
47
  this.optionalAuthMiddleware = (req, res, next) => {
51
48
  this.apiKeyMiddleware(req, res, () => {
52
49
  const authHeader = req.headers.authorization;
53
50
  if (!authHeader)
54
51
  return next();
55
- const token = authHeader.split(' ')[1];
52
+ const token = authHeader.split(" ")[1];
56
53
  if (!token)
57
54
  return next();
58
55
  try {
@@ -65,7 +62,7 @@ class APIRouter {
65
62
  });
66
63
  };
67
64
  this.normalizeRoleName = async (value) => {
68
- const rolesModel = this.getModel('roles'); // already available in your router
65
+ const rolesModel = this.getModel("roles");
69
66
  const isHex24 = (s) => /^[0-9a-fA-F]{24}$/.test(s);
70
67
  const fromString = async (s) => {
71
68
  if (!s)
@@ -73,17 +70,17 @@ class APIRouter {
73
70
  if (isHex24(s) && rolesModel) {
74
71
  const docs = await rolesModel.read({ id: s }, 1, 0, true);
75
72
  const n = docs?.[0]?.name;
76
- return typeof n === 'string' ? n : null;
73
+ return typeof n === "string" ? n : null;
77
74
  }
78
- return s; // assume literal role name like "admin"
75
+ return s;
79
76
  };
80
- if (typeof value === 'string')
77
+ if (typeof value === "string")
81
78
  return (await fromString(value))?.toLowerCase() ?? null;
82
79
  if (Array.isArray(value)) {
83
80
  for (const v of value) {
84
- const n = typeof v === 'string'
81
+ const n = typeof v === "string"
85
82
  ? await fromString(v)
86
- : v && typeof v === 'object' && typeof v.name === 'string'
83
+ : v && typeof v === "object" && typeof v.name === "string"
87
84
  ? v.name
88
85
  : null;
89
86
  if (n)
@@ -91,21 +88,22 @@ class APIRouter {
91
88
  }
92
89
  return null;
93
90
  }
94
- if (value && typeof value === 'object') {
91
+ if (value && typeof value === "object") {
95
92
  const o = value;
96
- if (typeof o.name === 'string')
93
+ if (typeof o.name === "string")
97
94
  return o.name.toLowerCase();
98
- if (typeof o.id === 'string') {
95
+ if (typeof o.id === "string") {
99
96
  const n = await fromString(o.id);
100
97
  return n ? n.toLowerCase() : null;
101
98
  }
102
99
  }
103
100
  return null;
104
101
  };
102
+ // ---------- auth middlewares ----------
105
103
  this.apiKeyMiddleware = (req, res, next) => {
106
- const apiKey = req.headers['x-api-key'];
107
- if (typeof apiKey !== 'string' || apiKey !== this.trustedApiKey) {
108
- return res.status(401).json({ error: 'Invalid or missing API key' });
104
+ const apiKey = req.headers["x-api-key"];
105
+ if (typeof apiKey !== "string" || apiKey !== this.trustedApiKey) {
106
+ return res.status(401).json({ error: "Invalid or missing API key" });
109
107
  }
110
108
  next();
111
109
  };
@@ -113,10 +111,10 @@ class APIRouter {
113
111
  this.apiKeyMiddleware(req, res, () => {
114
112
  const authHeader = req.headers.authorization;
115
113
  if (!authHeader)
116
- return res.status(401).json({ error: 'Authorization header missing' });
117
- const token = authHeader.split(' ')[1];
114
+ return res.status(401).json({ error: "Authorization header missing" });
115
+ const token = authHeader.split(" ")[1];
118
116
  if (!token)
119
- return res.status(401).json({ error: 'API Token missing.' });
117
+ return res.status(401).json({ error: "API Token missing." });
120
118
  try {
121
119
  const decoded = jsonwebtoken_1.default.verify(token, this.jwtSecret);
122
120
  // @ts-ignore
@@ -124,32 +122,68 @@ class APIRouter {
124
122
  next();
125
123
  }
126
124
  catch {
127
- res.status(401).json({ error: 'Invalid or expired token' });
125
+ res.status(401).json({ error: "Invalid or expired token" });
128
126
  }
129
127
  });
130
128
  };
131
- this.isDevelopment = process.env.APP_MODE !== 'production';
129
+ // ---------- helpers ----------
130
+ this.validateRequiredFields = (schema, payload, mode = "create") => {
131
+ const missing = [];
132
+ const isEmpty = (v) => v === undefined || v === null || (typeof v === "string" && v.trim() === "");
133
+ let baseKeys = null;
134
+ const baseName = schema?.extends;
135
+ if (baseName) {
136
+ try {
137
+ const baseLC = baseName.toLowerCase();
138
+ const baseSchema = this.getSchema(baseLC);
139
+ baseKeys = new Set(Object.keys(baseSchema.attributes || {}));
140
+ }
141
+ catch {
142
+ baseKeys = null;
143
+ }
144
+ }
145
+ for (const [key, attribute] of Object.entries(schema.attributes)) {
146
+ if (Array.isArray(attribute))
147
+ continue;
148
+ if (attribute?.private)
149
+ continue;
150
+ if (baseKeys && key !== "baseId" && baseKeys.has(key))
151
+ continue;
152
+ const reqd = Boolean(attribute?.required);
153
+ if (!reqd)
154
+ continue;
155
+ if (mode === "create") {
156
+ if (isEmpty(payload[key]))
157
+ missing.push(key);
158
+ }
159
+ else {
160
+ if (Object.prototype.hasOwnProperty.call(payload, key) && isEmpty(payload[key])) {
161
+ missing.push(key);
162
+ }
163
+ }
164
+ }
165
+ return missing;
166
+ };
167
+ this.isDevelopment = process.env.APP_MODE !== "production";
132
168
  this.router = express_1.default.Router();
133
169
  this.fileStorage = options.fileStorageAdapter;
134
- this.setupFileRoutes();
135
170
  this.dbAdapter = options.dbAdapter || new InMemoryAdapter_1.InMemoryAdapter();
136
- this.jwtSecret = process.env.JWT_SECRET || 'default_jwt_secret';
171
+ this.jwtSecret = process.env.JWT_SECRET || "default_jwt_secret";
137
172
  if (this.isDevelopment && options.server) {
138
173
  (0, SchemaService_1.startSchemaService)(options.server, {
139
174
  getApiKey: () => this.trustedApiKey,
140
175
  });
141
- options.server.on('listening', () => {
176
+ options.server.on("listening", () => {
142
177
  // @ts-ignore
143
178
  const addr = options.server.address();
144
- Logger_1.default.info('SchemaService', `[schema-service] started at /__nextmin__/schema ns /schema on ${typeof addr === 'string' ? addr : `${addr?.address}:${addr?.port}`}`);
179
+ Logger_1.default.info("SchemaService", `[schema-service] started at /__nextmin__/schema ns /schema on ${typeof addr === "string" ? addr : `${addr?.address}:${addr?.port}`}`);
145
180
  });
146
181
  }
147
- this.schemaLoader =
148
- SchemaLoader_1.SchemaLoader.getInstance?.() ?? new SchemaLoader_1.SchemaLoader();
182
+ this.schemaLoader = SchemaLoader_1.SchemaLoader.getInstance?.() ?? new SchemaLoader_1.SchemaLoader();
149
183
  const initialSchemas = this.schemaLoader.getSchemas();
150
184
  this.setLiveSchemas(initialSchemas);
151
185
  const finishBoot = async () => {
152
- if (typeof this.dbAdapter.registerSchemas === 'function') {
186
+ if (typeof this.dbAdapter.registerSchemas === "function") {
153
187
  await this.dbAdapter.registerSchemas(initialSchemas);
154
188
  }
155
189
  this.rebuildModels(Object.values(initialSchemas));
@@ -160,48 +194,47 @@ class APIRouter {
160
194
  const initializer = new DefaultDataInitializer_1.DefaultDataInitializer(this.dbAdapter, this.models);
161
195
  try {
162
196
  await initializer.initialize();
163
- this.trustedApiKey = initializer.getApiKey() || '';
164
- Logger_1.default.info('APIRouter', `Trusted API key set: ${this.trustedApiKey ? '[hidden]' : 'none'}`);
197
+ this.trustedApiKey = initializer.getApiKey() || "";
198
+ Logger_1.default.info("APIRouter", `Trusted API key set: ${this.trustedApiKey ? "[hidden]" : "none"}`);
165
199
  }
166
200
  catch (err) {
167
- Logger_1.default.error('APIRouter', 'Failed to initialize default data', err);
201
+ Logger_1.default.error("APIRouter", "Failed to initialize default data", err);
168
202
  }
169
203
  this.setupNotFoundMiddleware();
170
204
  this.wireSchemaHotReload();
171
205
  };
172
- if (typeof this.dbAdapter.registerSchemas === 'function') {
206
+ if (typeof this.dbAdapter.registerSchemas === "function") {
173
207
  const res = this.dbAdapter.registerSchemas(initialSchemas);
174
208
  if (res instanceof Promise) {
175
- res.then(finishBoot).catch((err) => {
176
- Logger_1.default.error('APIRouter', 'registerSchemas failed', err);
209
+ res
210
+ .then(finishBoot)
211
+ .catch((err) => {
212
+ Logger_1.default.error("APIRouter", "registerSchemas failed", err);
177
213
  this.setupNotFoundMiddleware();
178
214
  });
179
215
  return;
180
216
  }
181
217
  }
182
218
  finishBoot().catch((err) => {
183
- Logger_1.default.error('APIRouter', 'finishBoot error', err);
219
+ Logger_1.default.error("APIRouter", "finishBoot error", err);
184
220
  this.setupNotFoundMiddleware();
185
221
  });
186
222
  }
187
223
  getRouter() {
188
- // Avoid noisy logs on each getRouter() call
189
224
  return this.router;
190
225
  }
191
- // ---------------- Hot reload ----------------
226
+ // ---------- Hot reload ----------
192
227
  wireSchemaHotReload() {
193
228
  const anyLoader = this.schemaLoader;
194
- if (typeof anyLoader.on !== 'function')
229
+ if (typeof anyLoader.on !== "function")
195
230
  return;
196
- anyLoader.on('schemasChanged', async (newSchemas) => {
231
+ anyLoader.on("schemasChanged", async (newSchemas) => {
197
232
  try {
198
233
  const removed = this.diffRemovedModels(this.liveSchemas, newSchemas);
199
- if (removed.length &&
200
- typeof this.dbAdapter.unregisterSchemas === 'function') {
234
+ if (removed.length && typeof this.dbAdapter.unregisterSchemas === "function") {
201
235
  await this.dbAdapter.unregisterSchemas(removed);
202
236
  }
203
- else if (removed.length &&
204
- typeof this.dbAdapter.dropModel === 'function') {
237
+ else if (removed.length && typeof this.dbAdapter.dropModel === "function") {
205
238
  for (const name of removed) {
206
239
  try {
207
240
  await this.dbAdapter.dropModel(name);
@@ -209,24 +242,24 @@ class APIRouter {
209
242
  catch { }
210
243
  }
211
244
  }
212
- if (typeof this.dbAdapter.registerSchemas === 'function') {
245
+ if (typeof this.dbAdapter.registerSchemas === "function") {
213
246
  await this.dbAdapter.registerSchemas(newSchemas);
214
247
  }
215
248
  this.setLiveSchemas(newSchemas);
216
249
  this.rebuildModels(Object.values(newSchemas));
217
250
  this.mountRoutes(newSchemas);
218
251
  await this.syncAllIndexes(newSchemas);
219
- Logger_1.default.info('APIRouter', `Schemas reloaded (added/updated: ${Object.keys(newSchemas).length}, removed: ${removed.join(', ') || 'none'})`);
252
+ Logger_1.default.info("APIRouter", `Schemas reloaded (added/updated: ${Object.keys(newSchemas).length}, removed: ${removed.join(", ") || "none"})`);
220
253
  }
221
254
  catch (err) {
222
- Logger_1.default.error('APIRouter', 'Failed to refresh after schemasChanged', err);
255
+ Logger_1.default.error("APIRouter", "Failed to refresh after schemasChanged", err);
223
256
  }
224
257
  });
225
258
  }
226
259
  async syncAllIndexes(schemas) {
227
- if (typeof this.dbAdapter.syncIndexes !== 'function')
260
+ if (typeof this.dbAdapter.syncIndexes !== "function")
228
261
  return;
229
- const plan = this.schemaLoader.getIndexPlan(); // modelName -> { field: dir }
262
+ const plan = this.schemaLoader.getIndexPlan();
230
263
  for (const s of Object.values(schemas)) {
231
264
  const modelName = s.modelName;
232
265
  const spec = plan[modelName];
@@ -235,7 +268,7 @@ class APIRouter {
235
268
  await this.dbAdapter.syncIndexes(modelName, spec);
236
269
  }
237
270
  catch (err) {
238
- Logger_1.default.warn('APIRouter', `Index sync failed for ${modelName}: ${err?.message || err}`);
271
+ Logger_1.default.warn("APIRouter", `Index sync failed for ${modelName}: ${err?.message || err}`);
239
272
  }
240
273
  }
241
274
  }
@@ -247,7 +280,7 @@ class APIRouter {
247
280
  if (!next.has(name.toLowerCase()))
248
281
  removed.push(name.toLowerCase());
249
282
  }
250
- return removed.filter((n) => n !== 'users' && n !== 'roles');
283
+ return removed.filter((n) => n !== "users" && n !== "roles");
251
284
  }
252
285
  setLiveSchemas(schemas) {
253
286
  const idx = {};
@@ -262,12 +295,11 @@ class APIRouter {
262
295
  this.models[name] = new BaseModel_1.BaseModel(s, this.dbAdapter);
263
296
  }
264
297
  }
265
- // ---------------- Mounting ----------------
266
298
  mountSchemasEndpointOnce() {
267
299
  if (this.schemasRouteRegistered)
268
300
  return;
269
301
  this.schemasRouteRegistered = true;
270
- this.router.get('/_schemas', this.apiKeyMiddleware, (_req, res) => {
302
+ this.router.get("/_schemas", this.apiKeyMiddleware, (_req, res) => {
271
303
  res.json({
272
304
  success: true,
273
305
  data: this.schemaLoader.getPublicSchemaList(),
@@ -275,1177 +307,48 @@ class APIRouter {
275
307
  });
276
308
  }
277
309
  mountRoutes(schemas) {
278
- if (!this.authRoutesInitialized && this.liveSchemas['users']) {
279
- this.setupAuthRoutes();
310
+ if (!this.authRoutesInitialized && this.liveSchemas["users"]) {
311
+ (0, setupAuthRoutes_1.setupAuthRoutes)(this.createCtx());
280
312
  this.authRoutesInitialized = true;
281
313
  }
282
314
  for (const s of Object.values(schemas)) {
283
315
  const name = s.modelName.toLowerCase();
284
316
  if (this.registeredModels.has(name))
285
317
  continue;
286
- this.setupRoutes(name);
318
+ (0, mountCrudRoutes_1.mountCrudRoutes)(this.createCtx(), name);
287
319
  this.registeredModels.add(name);
288
320
  }
289
321
  this.ensureNotFoundLast();
290
322
  }
291
- // ---------------- Generic CRUD (policy-enforced) ----------------
292
- setupRoutes(modelNameLC) {
293
- const basePath = `/${modelNameLC}`;
294
- const mwCreate = this.pickAuthFor('create', modelNameLC);
295
- const mwRead = this.pickAuthFor('read', modelNameLC);
296
- const mwUpdate = this.pickAuthFor('update', modelNameLC);
297
- const mwDelete = this.pickAuthFor('delete', modelNameLC);
298
- const ctxFromReq = (req) => {
299
- const raw = req.user?.role;
300
- let roleStr = null;
301
- if (typeof raw === 'string') {
302
- roleStr = raw;
303
- }
304
- else if (Array.isArray(raw)) {
305
- const first = raw[0];
306
- if (typeof first === 'string')
307
- roleStr = first;
308
- else if (first &&
309
- typeof first === 'object' &&
310
- typeof first.name === 'string') {
311
- roleStr = first.name;
312
- }
313
- }
314
- else if (raw &&
315
- typeof raw === 'object' &&
316
- typeof raw.name === 'string') {
317
- roleStr = raw.name;
318
- }
319
- roleStr = roleStr ? roleStr.toLowerCase() : null;
320
- return {
321
- isAuthenticated: !!req.user,
322
- role: roleStr,
323
- userId: req.user?.id ?? req.user?._id ?? null,
324
- isSuperadmin: roleStr === 'superadmin',
325
- apiKeyOk: true,
326
- };
327
- };
328
- // -------------- CREATE --------------
329
- this.router.post(basePath, mwCreate, async (req, res) => {
330
- try {
331
- const schema = this.getSchema(modelNameLC);
332
- if (!schema.allowedMethods.create) {
333
- return res
334
- .status(405)
335
- .json({ error: true, message: 'Method not allowed' });
336
- }
337
- const model = this.getModel(modelNameLC);
338
- const ctx = ctxFromReq(req);
339
- const schemaPolicy = {
340
- allowedMethods: schema.allowedMethods,
341
- access: schema.access,
342
- };
343
- const cdec = (0, authorize_1.authorize)(modelNameLC, 'create', schemaPolicy, ctx);
344
- if (!cdec.allow) {
345
- return res.status(403).json({ error: true, message: 'forbidden' });
346
- }
347
- let payload = (0, authorize_1.mergeCreateDefaults)(req.body, cdec.createDefaults);
348
- (0, authorize_1.enforceRestrictions)(payload, cdec.restrictions, ctx);
349
- if (!cdec.exposePrivate) {
350
- payload = (0, authorize_1.stripWriteDeny)(payload, cdec.writeDeny);
351
- }
352
- // ✅ NEW: DB-agnostic coercion (time/date/range → canonical strings)
353
- payload = (0, fieldCodecs_1.coerceForStorage)(schema, payload);
354
- // CREATE: required fields must be present and non-empty (now in canonical shape)
355
- const missing = this.validateRequiredFields(schema, payload, 'create');
356
- if (missing.length) {
357
- return res.status(400).json({
358
- error: true,
359
- message: `Missing required fields: ${missing.join(', ')}`,
360
- });
361
- }
362
- const conflicts = await this.checkUniqueFields(schema, payload);
363
- if (conflicts && conflicts.length > 0) {
364
- return res.status(400).json({
365
- error: true,
366
- fields: conflicts.map((field) => ({
367
- field,
368
- error: true,
369
- message: `You cannot use this ${field}. It's already been used.`,
370
- })),
371
- });
372
- }
373
- // Hash password for Users creates
374
- if (modelNameLC === 'users' && payload.password && this.jwtSecret) {
375
- const salt = await bcrypt_1.default.genSalt(10);
376
- payload.password = await bcrypt_1.default.hash(String(payload.password) + this.jwtSecret, salt);
377
- }
378
- // Extended schema handling: split into base + child
379
- if (schema.extends) {
380
- const baseName = String(schema.extends);
381
- const baseLC = baseName.toLowerCase();
382
- const baseSchema = this.getSchema(baseLC);
383
- const baseModel = this.getModel(baseLC);
384
- // Partition payload by base attributes
385
- const basePayload = {};
386
- const childPayload = {};
387
- for (const [k, v] of Object.entries(payload)) {
388
- if (baseSchema.attributes[k])
389
- basePayload[k] = v;
390
- else
391
- childPayload[k] = v;
392
- }
393
- // Unique checks per collection
394
- const baseConflicts = await this.checkUniqueFields(baseSchema, basePayload);
395
- if (baseConflicts && baseConflicts.length > 0) {
396
- return res.status(400).json({
397
- error: true,
398
- fields: baseConflicts.map((field) => ({
399
- field,
400
- error: true,
401
- message: `You cannot use this ${field}. It's already been used.`,
402
- })),
403
- });
404
- }
405
- const childConflicts = await this.checkUniqueFields(schema, childPayload);
406
- if (childConflicts && childConflicts.length > 0) {
407
- return res.status(400).json({
408
- error: true,
409
- fields: childConflicts.map((field) => ({
410
- field,
411
- error: true,
412
- message: `You cannot use this ${field}. It's already been used.`,
413
- })),
414
- });
415
- }
416
- // Hash password if base is Users
417
- if (baseLC === 'users' && basePayload.password && this.jwtSecret) {
418
- const salt = await bcrypt_1.default.genSalt(10);
419
- basePayload.password = await bcrypt_1.default.hash(String(basePayload.password) + this.jwtSecret, salt);
420
- }
421
- // Create base first
422
- const baseCreated = await baseModel.create(basePayload);
423
- // Then create child linking to base
424
- const linkField = 'baseId';
425
- const childToCreate = { ...childPayload, [linkField]: baseCreated.id };
426
- const childCreated = await model.create(childToCreate);
427
- // Merge docs for response (child overrides base on conflicts)
428
- let resultDoc = { ...baseCreated, ...childCreated };
429
- delete resultDoc.baseId;
430
- if (cdec.exposePrivate && childCreated?.id) {
431
- // Re-fetch both with private if needed
432
- const [refChild] = await model.read({ id: childCreated.id }, 1, 0, true);
433
- const [refBase] = await baseModel.read({ id: baseCreated.id }, 1, 0, true);
434
- if (refChild && refBase) {
435
- resultDoc = { ...refBase, ...refChild };
436
- delete resultDoc.baseId;
437
- }
438
- }
439
- const masked = cdec.exposePrivate
440
- ? (0, authorize_1.applyReadMaskOne)(resultDoc, cdec.sensitiveMask)
441
- : (0, authorize_1.applyReadMaskOne)(resultDoc, cdec.readMask);
442
- return res.status(201).json({
443
- success: true,
444
- message: `${schema.modelName} has been created successfully.`,
445
- data: masked,
446
- });
447
- }
448
- // Default (non-extended)
449
- const created = await model.create(payload);
450
- let resultDoc = created;
451
- if (cdec.exposePrivate && created?.id) {
452
- const refetched = await model.read({ id: created.id }, 1, 0, true);
453
- if (refetched?.[0])
454
- resultDoc = refetched[0];
455
- }
456
- const masked = cdec.exposePrivate
457
- ? (0, authorize_1.applyReadMaskOne)(resultDoc, cdec.sensitiveMask)
458
- : (0, authorize_1.applyReadMaskOne)(resultDoc, cdec.readMask);
459
- return res.status(201).json({
460
- success: true,
461
- message: `${schema.modelName} has been created successfully.`,
462
- data: masked,
463
- });
464
- }
465
- catch (error) {
466
- if (error?.code === 'MODEL_REMOVED') {
467
- return res
468
- .status(410)
469
- .json({ error: true, message: error.message });
470
- }
471
- this.handleWriteError(error, res);
472
- }
473
- });
474
- // -------------- READ (list) --------------
475
- this.router.get(basePath, mwRead, async (req, res) => {
476
- try {
477
- const schema = this.getSchema(modelNameLC);
478
- if (!schema.allowedMethods.read) {
479
- return res
480
- .status(405)
481
- .json({ error: true, message: 'Method not allowed' });
482
- }
483
- const model = this.getModel(modelNameLC);
484
- const page = Number.parseInt(String(req.query.page ?? '0'), 10) || 0;
485
- const limit = Number.parseInt(String(req.query.limit ?? '10'), 10) || 10;
486
- // ---- search params (extended) ----
487
- const q = String(req.query.q ?? '').trim();
488
- const searchKey = String(req.query.searchKey ?? '').trim();
489
- const searchKeysCSV = String(req.query.searchKeys ?? '').trim();
490
- const searchMode = /^(and|or)$/i.test(String(req.query.searchMode ?? ''))
491
- ? String(req.query.searchMode).toLowerCase()
492
- : 'or';
493
- const dateFromStr = String(req.query.dateFrom ?? '').trim();
494
- const dateToStr = String(req.query.dateTo ?? '').trim();
495
- let dateKey = String(req.query.dateKey ?? 'createdAt').trim();
496
- // ---- NEW: sort params (supports CSV for multi-field) ----
497
- const sortRaw = String(req.query.sort ?? '').trim(); // e.g. "name,createdAt"
498
- const sortTypeRaw = String(req.query.sortType ?? '').trim(); // e.g. "asc,desc"
499
- const filter = {};
500
- const splitCSV = (raw) => raw
501
- .split(',')
502
- .map((s) => s.trim())
503
- .filter(Boolean);
504
- // Build sort spec limited to schema fields. Default desc(createdAt) if nothing valid provided.
505
- const buildSortSpec = () => {
506
- const keys = splitCSV(sortRaw);
507
- const dirs = splitCSV(sortTypeRaw);
508
- const sort = {};
509
- keys.forEach((k, i) => {
510
- // keep the guard so unknown fields are ignored
511
- if (!schema.attributes?.[k])
512
- return;
513
- const d = dirs[i] ?? dirs[dirs.length - 1] ?? 'desc';
514
- sort[k] = /^(desc|-1)$/i.test(d) ? -1 : 1;
515
- });
516
- if (!Object.keys(sort).length) {
517
- // ✅ always default
518
- sort.createdAt = -1;
519
- }
520
- return sort;
521
- };
522
- // ---- SEARCH (legacy + multi) ----
523
- const ors = [];
524
- const ands = [];
525
- const buildPredicateForField = (field, attr, raw) => {
526
- const isArray = Array.isArray(attr);
527
- const base = isArray ? attr[0] : attr;
528
- const attrType = this.normalizeAttrType(base);
529
- const tokens = splitCSV(raw);
530
- switch (attrType) {
531
- case 'string':
532
- return isArray
533
- ? { [field]: { $in: tokens.length ? tokens : [raw] } }
534
- : { [field]: { $regex: raw, $options: 'i' } };
535
- case 'number': {
536
- const nums = tokens.map(Number).filter((n) => !Number.isNaN(n));
537
- return isArray
538
- ? nums.length
539
- ? { [field]: { $in: nums } }
540
- : null
541
- : nums.length
542
- ? { [field]: nums[0] }
543
- : null;
544
- }
545
- case 'boolean': {
546
- const toBool = (t) => /^(true|1|yes)$/i.test(t)
547
- ? true
548
- : /^(false|0|no)$/i.test(t)
549
- ? false
550
- : null;
551
- if (isArray) {
552
- const bools = tokens
553
- .map(toBool)
554
- .filter((v) => v !== null);
555
- return bools.length ? { [field]: { $in: bools } } : null;
556
- }
557
- const b = toBool(raw);
558
- return b === null ? null : { [field]: b };
559
- }
560
- case 'objectid':
561
- return isArray || tokens.length > 1
562
- ? { [field]: { $in: tokens } }
563
- : raw
564
- ? { [field]: raw }
565
- : null;
566
- case 'date': {
567
- const toDate = (t) => {
568
- const d = new Date(t);
569
- return Number.isNaN(+d) ? null : d;
570
- };
571
- if (isArray) {
572
- const ds = tokens.map(toDate).filter((d) => !!d);
573
- return ds.length ? { [field]: { $in: ds } } : null;
574
- }
575
- const d = toDate(raw);
576
- return d ? { [field]: d } : null;
577
- }
578
- default:
579
- return isArray
580
- ? { [field]: { $in: tokens.length ? tokens : [raw] } }
581
- : { [field]: raw };
582
- }
583
- };
584
- if (q && searchKey && schema.attributes?.[searchKey]) {
585
- const p = buildPredicateForField(searchKey, schema.attributes[searchKey], q);
586
- if (p)
587
- ors.push(p);
588
- }
589
- if (q && searchKeysCSV) {
590
- const keys = splitCSV(searchKeysCSV);
591
- const preds = keys
592
- .filter((k) => !!schema.attributes?.[k])
593
- .map((k) => buildPredicateForField(k, schema.attributes[k], q))
594
- .filter(Boolean);
595
- if (preds.length) {
596
- if (searchMode === 'and')
597
- ands.push(...preds);
598
- else
599
- ors.push(...preds);
600
- }
601
- }
602
- if (ands.length && ors.length)
603
- filter.$and = [...ands, { $or: ors }];
604
- else if (ands.length)
605
- filter.$and = [...(filter.$and || []), ...ands];
606
- else if (ors.length)
607
- filter.$or = [...(filter.$or || []), ...ors];
608
- // ---- DATE RANGE (unchanged) ----
609
- if (dateFromStr || dateToStr) {
610
- if (!schema.attributes?.[dateKey]) {
611
- if (schema.attributes?.createdAt)
612
- dateKey = 'createdAt';
613
- else if (schema.attributes?.updatedAt)
614
- dateKey = 'updatedAt';
615
- }
616
- if (dateKey && schema.attributes?.[dateKey]) {
617
- const range = {};
618
- if (dateFromStr) {
619
- const d = new Date(dateFromStr);
620
- if (!Number.isNaN(+d))
621
- range.$gte = d;
622
- }
623
- if (dateToStr) {
624
- const d = new Date(dateToStr);
625
- if (!Number.isNaN(+d))
626
- range.$lte = d;
627
- }
628
- if (Object.keys(range).length)
629
- filter[dateKey] = range;
630
- }
631
- }
632
- const ctx = ctxFromReq(req);
633
- const schemaPolicy = {
634
- allowedMethods: schema.allowedMethods,
635
- access: schema.access,
636
- };
637
- const rdec = (0, authorize_1.authorize)(modelNameLC, 'read', schemaPolicy, ctx);
638
- if (!rdec.allow)
639
- return res.status(403).json({ error: true, message: 'forbidden' });
640
- const finalFilter = (0, authorize_1.andFilter)(filter, rdec.queryFilter || {});
641
- const isUsersModel = modelNameLC === 'user' ||
642
- modelNameLC === 'users' ||
643
- (schema?.modelName ?? '').toLowerCase() === 'users';
644
- const currentUserId = req?.user?.id;
645
- // ---- NEW: compute sort spec
646
- const sort = buildSortSpec();
647
- // count
648
- const totalRows = await model.count(finalFilter);
649
- // try DB-level sort (preferred). Fallback to in-memory if adapter lacks sort support.
650
- let rawRows = [];
651
- // Ensure we can hydrate: baseId is private, so read with private when extended
652
- const needPrivateForHydrate = !!schema.extends;
653
- const exposePrivateForRead = needPrivateForHydrate || !!rdec.exposePrivate;
654
- try {
655
- rawRows = await model.read(finalFilter, limit + 1, page * limit, exposePrivateForRead, { sort });
656
- }
657
- catch {
658
- rawRows = await model.read(finalFilter, limit + 1, page * limit, exposePrivateForRead);
659
- if (sort && Object.keys(sort).length) {
660
- const orderKeys = Object.keys(sort);
661
- rawRows.sort((a, b) => {
662
- for (const k of orderKeys) {
663
- const dir = sort[k];
664
- const av = a?.[k];
665
- const bv = b?.[k];
666
- const ax = av instanceof Date ? +av : (av?.toString?.() ?? av);
667
- const bx = bv instanceof Date ? +bv : (bv?.toString?.() ?? bv);
668
- if (ax == null && bx == null)
669
- continue;
670
- if (ax == null)
671
- return 1 * dir;
672
- if (bx == null)
673
- return -1 * dir;
674
- if (ax > bx)
675
- return 1 * dir;
676
- if (ax < bx)
677
- return -1 * dir;
678
- }
679
- return 0;
680
- });
681
- }
682
- }
683
- let rows = currentUserId && isUsersModel
684
- ? rawRows
685
- .filter((r) => String(r?.id) !== String(currentUserId))
686
- .slice(0, limit)
687
- : rawRows.slice(0, limit);
688
- // If extended, hydrate with base data using baseId
689
- if (schema.extends) {
690
- const baseName = String(schema.extends);
691
- const baseLC = baseName.toLowerCase();
692
- const baseModel = this.getModel(baseLC);
693
- const hydrated = [];
694
- for (const row of rows) {
695
- const baseId = row?.baseId;
696
- if (baseId) {
697
- const [baseDoc] = await baseModel.read({ id: baseId }, 1, 0, !!rdec.exposePrivate);
698
- if (baseDoc) {
699
- const merged = { ...baseDoc, ...row };
700
- delete merged.baseId;
701
- hydrated.push(merged);
702
- continue;
703
- }
704
- }
705
- const fallback = { ...row };
706
- delete fallback.baseId;
707
- hydrated.push(fallback);
708
- }
709
- rows = hydrated;
710
- }
711
- const data = rdec.exposePrivate
712
- ? (0, authorize_1.applyReadMaskMany)(rows, rdec.sensitiveMask)
713
- : (0, authorize_1.applyReadMaskMany)(rows, rdec.readMask);
714
- return res.status(200).json({
715
- success: true,
716
- message: `Data fetched for ${schema.modelName}`,
717
- data,
718
- pagination: { totalRows, page, limit },
719
- sort, // optional echo
720
- });
721
- }
722
- catch (error) {
723
- if (error?.code === 'MODEL_REMOVED') {
724
- return res.status(410).json({ error: true, message: error.message });
725
- }
726
- return res.status(400).json({ error: true, message: error.message });
727
- }
728
- });
729
- // -------------- READ (by id) --------------
730
- this.router.get(`${basePath}/:id`, mwRead, async (req, res) => {
731
- try {
732
- const schema = this.getSchema(modelNameLC);
733
- if (!schema.allowedMethods.read) {
734
- return res
735
- .status(405)
736
- .json({ error: true, message: 'Method not allowed' });
737
- }
738
- const model = this.getModel(modelNameLC);
739
- const ctx = ctxFromReq(req);
740
- const schemaPolicy = {
741
- allowedMethods: schema.allowedMethods,
742
- access: schema.access,
743
- };
744
- const decForInclude = (0, authorize_1.authorize)(modelNameLC, 'read', schemaPolicy, ctx);
745
- // Ensure we can hydrate: baseId is private, so read with private when extended
746
- const needPrivateForHydrate2 = !!schema.extends;
747
- const exposePrivateForRead2 = needPrivateForHydrate2 || !!decForInclude.exposePrivate;
748
- const recordArr = await model.read({ id: req.params.id }, 1, 0, exposePrivateForRead2);
749
- const doc = recordArr?.[0];
750
- if (!doc) {
751
- return res
752
- .status(404)
753
- .json({ error: true, message: `${schema.modelName} not found` });
754
- }
755
- const dec = (0, authorize_1.authorize)(modelNameLC, 'read', schemaPolicy, ctx, doc);
756
- if (!dec.allow)
757
- return res.status(403).json({ error: true, message: 'forbidden' });
758
- let toReturn = doc;
759
- if (schema.extends) {
760
- const baseName = String(schema.extends);
761
- const baseLC = baseName.toLowerCase();
762
- const baseModel = this.getModel(baseLC);
763
- const baseId = doc?.baseId;
764
- if (baseId) {
765
- const [baseDoc] = await baseModel.read({ id: baseId }, 1, 0, !!dec.exposePrivate);
766
- if (baseDoc) {
767
- toReturn = { ...baseDoc, ...doc };
768
- delete toReturn.baseId;
769
- }
770
- }
771
- }
772
- const data = dec.exposePrivate
773
- ? (0, authorize_1.applyReadMaskOne)(toReturn, dec.sensitiveMask)
774
- : (0, authorize_1.applyReadMaskOne)(toReturn, dec.readMask);
775
- return res.status(200).json({
776
- success: true,
777
- message: `${schema.modelName} found`,
778
- data,
779
- });
780
- }
781
- catch (error) {
782
- if (error?.code === 'MODEL_REMOVED')
783
- return res
784
- .status(410)
785
- .json({ error: true, message: error.message });
786
- Logger_1.default.error('error', error.message);
787
- res.status(400).json({ error: true, message: error.message });
788
- }
789
- });
790
- // -------------- UPDATE --------------
791
- this.router.put(`${basePath}/:id`, mwUpdate, async (req, res) => {
792
- try {
793
- const schema = this.getSchema(modelNameLC);
794
- if (!schema.allowedMethods.update) {
795
- return res
796
- .status(405)
797
- .json({ error: true, message: 'Method not allowed' });
798
- }
799
- const model = this.getModel(modelNameLC);
800
- const beforeArr = await model.read({ id: req.params.id }, 1, 0, true);
801
- const before = beforeArr?.[0];
802
- const ctx = ctxFromReq(req);
803
- const schemaPolicy = {
804
- allowedMethods: schema.allowedMethods,
805
- access: schema.access,
806
- };
807
- const udec = (0, authorize_1.authorize)(modelNameLC, 'update', schemaPolicy, ctx, before);
808
- if (!udec.allow) {
809
- return res.status(403).json({ error: true, message: 'forbidden' });
810
- }
811
- let upd = { ...req.body };
812
- (0, authorize_1.enforceRestrictions)(upd, udec.restrictions, ctx);
813
- if (!udec.exposePrivate) {
814
- upd = (0, authorize_1.stripWriteDeny)(upd, udec.writeDeny);
815
- }
816
- // ✅ NEW: DB-agnostic coercion (time/date/range → canonical strings)
817
- upd = (0, fieldCodecs_1.coerceForStorage)(schema, upd);
818
- // Users: handle password if (and only if) provided
819
- if (modelNameLC === 'users' &&
820
- Object.prototype.hasOwnProperty.call(upd, 'password')) {
821
- if (!upd.password) {
822
- delete upd.password; // ignore empty → do not overwrite
823
- }
824
- else if (this.jwtSecret) {
825
- const salt = await bcrypt_1.default.genSalt(10);
826
- upd.password = await bcrypt_1.default.hash(String(upd.password) + this.jwtSecret, salt);
827
- }
828
- }
829
- // UPDATE: validate only fields present in payload (now in canonical shape)
830
- const missing = this.validateRequiredFields(schema, upd, 'update');
831
- if (missing.length) {
832
- return res.status(400).json({
833
- error: true,
834
- message: `Missing required fields: ${missing.join(', ')}`,
835
- });
836
- }
837
- // Extended schema update handling
838
- if (schema.extends) {
839
- const baseName = String(schema.extends);
840
- const baseLC = baseName.toLowerCase();
841
- const baseSchema = this.getSchema(baseLC);
842
- const baseModel = this.getModel(baseLC);
843
- const baseId = before?.baseId;
844
- if (!baseId) {
845
- return res.status(400).json({ error: true, message: 'Invalid extended record: missing baseId' });
846
- }
847
- // Partition update
848
- const baseUpd = {};
849
- const childUpd = {};
850
- for (const [k, v] of Object.entries(upd)) {
851
- if (k === 'baseId')
852
- continue; // never allow changing link
853
- if (baseSchema.attributes[k])
854
- baseUpd[k] = v;
855
- else
856
- childUpd[k] = v;
857
- }
858
- // Unique checks per collection
859
- const baseConflicts = await this.checkUniqueFields(baseSchema, baseUpd, String(baseId));
860
- if (baseConflicts && baseConflicts.length > 0) {
861
- return res.status(400).json({
862
- error: true,
863
- message: 'There are some error while updating record.',
864
- fields: baseConflicts.map((field) => ({
865
- field,
866
- error: true,
867
- message: `You cannot use this ${field}. It's already been used.`,
868
- })),
869
- });
870
- }
871
- const childConflicts = await this.checkUniqueFields(schema, childUpd, req.params.id);
872
- if (childConflicts && childConflicts.length > 0) {
873
- return res.status(400).json({
874
- error: true,
875
- message: 'There are some error while updating record.',
876
- fields: childConflicts.map((field) => ({
877
- field,
878
- error: true,
879
- message: `You cannot use this ${field}. It's already been used.`,
880
- })),
881
- });
882
- }
883
- // Hash password if base is Users and provided
884
- if (baseLC === 'users' &&
885
- Object.prototype.hasOwnProperty.call(baseUpd, 'password')) {
886
- if (!baseUpd.password) {
887
- delete baseUpd.password;
888
- }
889
- else if (this.jwtSecret) {
890
- const salt = await bcrypt_1.default.genSalt(10);
891
- baseUpd.password = await bcrypt_1.default.hash(String(baseUpd.password) + this.jwtSecret, salt);
892
- }
893
- }
894
- // Perform updates
895
- let updatedChild = before;
896
- if (Object.keys(childUpd).length) {
897
- updatedChild = await model.update(req.params.id, childUpd);
898
- }
899
- if (Object.keys(baseUpd).length) {
900
- await baseModel.update(String(baseId), baseUpd);
901
- }
902
- // Re-read and merge
903
- const [refChild] = await model.read({ id: updatedChild.id }, 1, 0, true);
904
- const [refBase] = await baseModel.read({ id: String(baseId) }, 1, 0, true);
905
- let responseDoc = refChild && refBase ? { ...refBase, ...refChild } : updatedChild;
906
- if (responseDoc)
907
- delete responseDoc.baseId;
908
- const masked = udec.exposePrivate
909
- ? (0, authorize_1.applyReadMaskOne)(responseDoc, udec.sensitiveMask)
910
- : (0, authorize_1.applyReadMaskOne)(responseDoc, udec.readMask);
911
- return res.json(masked);
912
- }
913
- // Default (non-extended)
914
- const conflicts = await this.checkUniqueFields(schema, upd, req.params.id);
915
- if (conflicts && conflicts.length > 0) {
916
- return res.status(400).json({
917
- error: true,
918
- message: 'There are some error while updating record.',
919
- fields: conflicts.map((field) => ({
920
- field,
921
- error: true,
922
- message: `You cannot use this ${field}. It's already been used.`,
923
- })),
924
- });
925
- }
926
- const updatedRecord = await model.update(req.params.id, upd);
927
- let responseDoc = updatedRecord;
928
- if (udec.exposePrivate && updatedRecord?.id) {
929
- const refetched = await model.read({ id: updatedRecord.id }, 1, 0, true);
930
- if (refetched?.[0])
931
- responseDoc = refetched[0];
932
- }
933
- const masked = udec.exposePrivate
934
- ? (0, authorize_1.applyReadMaskOne)(responseDoc, udec.sensitiveMask)
935
- : (0, authorize_1.applyReadMaskOne)(responseDoc, udec.readMask);
936
- return res.json(masked);
937
- }
938
- catch (error) {
939
- if (error?.code === 'MODEL_REMOVED') {
940
- return res
941
- .status(410)
942
- .json({ error: true, message: error.message });
943
- }
944
- this.handleWriteError(error, res);
945
- }
946
- });
947
- // -------------- DELETE --------------
948
- this.router.delete(`${basePath}/:id`, mwDelete, async (req, res) => {
949
- try {
950
- const schema = this.getSchema(modelNameLC);
951
- if (!schema.allowedMethods.delete) {
952
- return res
953
- .status(405)
954
- .json({ error: true, message: 'Method not allowed' });
955
- }
956
- const model = this.getModel(modelNameLC);
957
- const docArr = await model.read({ id: req.params.id }, 1, 0, true);
958
- const doc = docArr?.[0];
959
- const ctx = ctxFromReq(req);
960
- const schemaPolicy = {
961
- allowedMethods: schema.allowedMethods,
962
- access: schema.access,
963
- };
964
- const ddec = (0, authorize_1.authorize)(modelNameLC, 'delete', schemaPolicy, ctx, doc);
965
- if (!ddec.allow)
966
- return res.status(403).json({ error: true, message: 'forbidden' });
967
- // Extended delete: remove child then base
968
- if (schema.extends) {
969
- const baseName = String(schema.extends);
970
- const baseLC = baseName.toLowerCase();
971
- const baseModel = this.getModel(baseLC);
972
- const baseId = doc?.baseId;
973
- const deletedChild = await model.delete(req.params.id);
974
- if (baseId) {
975
- try {
976
- await baseModel.delete(String(baseId));
977
- }
978
- catch (e) {
979
- // ignore if already missing
980
- }
981
- }
982
- const merged = baseId
983
- ? (() => {
984
- // Try to include base snapshot for response if available
985
- return { ...deletedChild, baseId: undefined };
986
- })()
987
- : deletedChild;
988
- const masked = ddec.exposePrivate
989
- ? (0, authorize_1.applyReadMaskOne)(merged, ddec.sensitiveMask)
990
- : (0, authorize_1.applyReadMaskOne)(merged, ddec.readMask);
991
- return res.json({
992
- success: true,
993
- message: `We have deleted the record successfully.`,
994
- data: masked,
995
- });
996
- }
997
- const deletedRecord = await model.delete(req.params.id);
998
- if (!deletedRecord) {
999
- return res
1000
- .status(404)
1001
- .json({ error: true, message: `${schema.modelName} not found` });
1002
- }
1003
- const masked = ddec.exposePrivate
1004
- ? (0, authorize_1.applyReadMaskOne)(deletedRecord, ddec.sensitiveMask)
1005
- : (0, authorize_1.applyReadMaskOne)(deletedRecord, ddec.readMask);
1006
- return res.json({
1007
- success: true,
1008
- message: `We have deleted the record successfully.`,
1009
- data: masked,
1010
- });
1011
- }
1012
- catch (error) {
1013
- if (error?.code === 'MODEL_REMOVED')
1014
- return res
1015
- .status(410)
1016
- .json({ error: true, message: error.message });
1017
- res.status(400).json({ error: true, message: error.message });
1018
- }
1019
- });
1020
- }
1021
323
  mountFindRoutes() {
1022
324
  if (this.findRoutesMounted)
1023
325
  return;
1024
326
  this.findRoutesMounted = true;
1025
- // ---------- helpers ----------
1026
- const parseSort = (expr) => {
1027
- if (!expr)
1028
- return;
1029
- const out = {};
1030
- for (const raw of expr
1031
- .split(',')
1032
- .map((s) => s.trim())
1033
- .filter(Boolean)) {
1034
- if (raw.startsWith('-'))
1035
- out[raw.slice(1)] = -1;
1036
- else if (raw.startsWith('+'))
1037
- out[raw.slice(1)] = 1;
1038
- else
1039
- out[raw] = 1;
1040
- }
1041
- return Object.keys(out).length ? out : undefined;
1042
- };
1043
- const parseQuery = (req) => {
1044
- const limit = Math.min(parseInt(String(req.query.limit ?? '12'), 10) || 12, 100);
1045
- const page = Math.max(parseInt(String(req.query.page ?? '1'), 10) || 1, 1);
1046
- const skip = (page - 1) * limit;
1047
- const fields = String(req.query.fields ?? '')
1048
- .split(',')
1049
- .map((s) => s.trim())
1050
- .filter(Boolean);
1051
- const projection = fields.length
1052
- ? Object.fromEntries(fields.map((f) => [f, 1]))
1053
- : undefined;
1054
- const sort = parseSort(String(req.query.sort ?? '-createdAt'));
1055
- return { limit, page, skip, projection, sort };
1056
- };
1057
- // normalize possible values into an array of string ids
1058
- const extractIds = (val) => {
1059
- if (val == null)
1060
- return [];
1061
- const arr = Array.isArray(val) ? val : [val];
1062
- const toId = (v) => {
1063
- if (!v)
1064
- return null;
1065
- if (typeof v === 'string')
1066
- return v; // "6523..."
1067
- if (typeof v === 'number')
1068
- return String(v);
1069
- if (typeof v === 'object') {
1070
- if (typeof v.id === 'string')
1071
- return v.id; // populated doc.id
1072
- if (v._id && typeof v._id === 'string')
1073
- return v._id;
1074
- if (v._id && typeof v._id.toString === 'function')
1075
- return v._id.toString();
1076
- }
1077
- return null;
1078
- };
1079
- return arr.map(toId).filter((s) => !!s);
1080
- };
1081
- // minimal ctx (same logic as setupRoutes)
1082
- const ctxFromReq = (req) => {
1083
- const raw = req.user?.role;
1084
- let roleStr = null;
1085
- if (typeof raw === 'string')
1086
- roleStr = raw;
1087
- else if (Array.isArray(raw)) {
1088
- const first = raw[0];
1089
- if (typeof first === 'string')
1090
- roleStr = first;
1091
- else if (first &&
1092
- typeof first === 'object' &&
1093
- typeof first.name === 'string') {
1094
- roleStr = first.name;
1095
- }
1096
- }
1097
- else if (raw &&
1098
- typeof raw === 'object' &&
1099
- typeof raw.name === 'string') {
1100
- roleStr = raw.name;
1101
- }
1102
- roleStr = roleStr ? roleStr.toLowerCase() : null;
1103
- return {
1104
- isAuthenticated: !!req.user,
1105
- role: roleStr,
1106
- userId: req.user?.id ?? req.user?._id ?? null,
1107
- isSuperadmin: roleStr === 'superadmin',
1108
- apiKeyOk: true,
1109
- };
1110
- };
1111
- // detect { type: "ObjectId"/"ref" } or array of { type: "ObjectId", ref }
1112
- const refInfoFromAttr = (attr) => {
1113
- if (!attr)
1114
- return null;
1115
- // array style from your JSON schema
1116
- if (Array.isArray(attr) && attr[0] && attr[0].ref) {
1117
- return { ref: String(attr[0].ref), isArray: true };
1118
- }
1119
- const a = Array.isArray(attr) ? attr?.[0] : attr;
1120
- const t = (typeof a?.type === 'string' ? a.type : String(a?.type || '')).toLowerCase();
1121
- if (a?.ref && (t === 'objectid' || t === 'ref')) {
1122
- return { ref: String(a.ref), isArray: false };
1123
- }
1124
- return null;
1125
- };
1126
- // try adapter.findOne with projection to avoid autopopulate; fallback to model.read
1127
- const safeGetContainerRefIDs = async (containerModelLC, id, refField) => {
1128
- const containerSchema = this.getSchema(containerModelLC);
1129
- const adapterAny = this.dbAdapter;
1130
- if (typeof adapterAny.findOne === 'function') {
1131
- try {
1132
- // adapter should map id → db PK correctly (e.g., _id for Mongo)
1133
- const raw = await adapterAny.findOne(containerSchema.modelName, { id }, { projection: { [refField]: 1 } });
1134
- return extractIds(raw?.[refField]);
1135
- }
1136
- catch {
1137
- // fall through
1138
- }
1139
- }
1140
- // fallback: BaseModel.read (may autopopulate, so we still normalize)
1141
- const containerDoc = (await this.getModel(containerModelLC).read({ id }, 1, 0, true))?.[0];
1142
- return extractIds(containerDoc?.[refField]);
1143
- };
1144
- // ---------- FORWARD ----------
1145
- // /find/:container/:refField/:id
1146
- this.router.get('/find/:container/:refField/:id', this.optionalAuthMiddleware, async (req, res) => {
1147
- try {
1148
- const containerLC = String(req.params.container || '').toLowerCase();
1149
- const refField = String(req.params.refField || '');
1150
- const id = String(req.params.id || '');
1151
- const schema = this.getSchema(containerLC); // ensure container exists
1152
- const attr = schema.attributes?.[refField];
1153
- const rinfo = refInfoFromAttr(attr);
1154
- if (!rinfo) {
1155
- return res.status(400).json({
1156
- error: true,
1157
- message: 'refField is not a reference field',
1158
- });
1159
- }
1160
- const { limit, page, skip, projection, sort } = parseQuery(req);
1161
- // policy on container read
1162
- const ctx = ctxFromReq(req);
1163
- const cPolicy = {
1164
- allowedMethods: schema.allowedMethods,
1165
- access: schema.access,
1166
- };
1167
- const cDec = (0, authorize_1.authorize)(containerLC, 'read', cPolicy, ctx);
1168
- if (!cDec.allow)
1169
- return res.status(403).json({ error: true, message: 'forbidden' });
1170
- // get raw ids safely (no autopopulate dependency)
1171
- const ids = await safeGetContainerRefIDs(containerLC, id, refField);
1172
- const targetLC = String(rinfo.ref || '').toLowerCase();
1173
- const targetSchema = this.getSchema(targetLC);
1174
- const tPolicy = {
1175
- allowedMethods: targetSchema.allowedMethods,
1176
- access: targetSchema.access,
1177
- };
1178
- const tDec = (0, authorize_1.authorize)(targetLC, 'read', tPolicy, ctx);
1179
- if (!tDec.allow)
1180
- return res.status(403).json({ error: true, message: 'forbidden' });
1181
- if (ids.length === 0) {
1182
- return res.status(200).json({
1183
- success: true,
1184
- message: `Data fetched for ${targetSchema.modelName}`,
1185
- data: [],
1186
- pagination: { totalRows: 0, page, limit },
1187
- });
1188
- }
1189
- const adapterAny = this.dbAdapter;
1190
- let items = [];
1191
- if (typeof adapterAny.findMany === 'function') {
1192
- items = await adapterAny.findMany(targetSchema.modelName, { id: { $in: ids } }, // DB-agnostic: adapter maps id → PK
1193
- { sort, skip, limit, projection });
1194
- }
1195
- else {
1196
- // fallback (BaseModel.read); note: no projection/sort
1197
- items = await this.getModel(targetLC).read({ id: { $in: ids } }, limit, skip, !!tDec.exposePrivate);
1198
- }
1199
- // If extended, hydrate with base data using baseId before masking
1200
- if (targetSchema.extends && Array.isArray(items) && items.length) {
1201
- const baseName = String(targetSchema.extends);
1202
- const baseLC = baseName.toLowerCase();
1203
- const baseModel = this.getModel(baseLC);
1204
- const baseIds = Array.from(new Set(items.map((it) => it?.baseId).filter(Boolean)));
1205
- if (baseIds.length) {
1206
- const baseDocs = await baseModel.read({ id: { $in: baseIds } }, baseIds.length, 0, !!tDec.exposePrivate);
1207
- const baseMap = new Map(baseDocs.map((b) => [String(b?.id ?? b?._id), b]));
1208
- items = items.map((it) => {
1209
- const bid = String(it?.baseId || '');
1210
- const b = bid ? baseMap.get(bid) : null;
1211
- const merged = b ? { ...b, ...it } : { ...it };
1212
- delete merged.baseId;
1213
- return merged;
1214
- });
1215
- }
1216
- else {
1217
- items = items.map((it) => {
1218
- const copy = { ...it };
1219
- delete copy.baseId;
1220
- return copy;
1221
- });
1222
- }
1223
- }
1224
- const masked = tDec.exposePrivate
1225
- ? (0, authorize_1.applyReadMaskMany)(items, tDec.sensitiveMask)
1226
- : (0, authorize_1.applyReadMaskMany)(items, tDec.readMask);
1227
- const totalRows = ids.length;
1228
- return res.status(200).json({
1229
- success: true,
1230
- message: `Data fetched for ${targetSchema.modelName}`,
1231
- data: masked,
1232
- pagination: { totalRows, page, limit },
1233
- });
1234
- }
1235
- catch (err) {
1236
- if (err?.code === 'MODEL_REMOVED') {
1237
- return res.status(410).json({ error: true, message: err.message });
1238
- }
1239
- return res
1240
- .status(400)
1241
- .json({ error: true, message: err?.message || 'Error' });
1242
- }
1243
- });
1244
- // ---------- REVERSE ----------
1245
- // /find/reverse/:target/:byField/:id
1246
- this.router.get('/find/reverse/:target/:byField/:id', this.optionalAuthMiddleware, async (req, res) => {
1247
- try {
1248
- const targetLC = String(req.params.target || '').toLowerCase();
1249
- const byField = String(req.params.byField || '');
1250
- const id = String(req.params.id || '');
1251
- const targetSchema = this.getSchema(targetLC);
1252
- const attr = targetSchema.attributes?.[byField];
1253
- const rinfo = refInfoFromAttr(attr);
1254
- if (!rinfo) {
1255
- return res.status(400).json({
1256
- error: true,
1257
- message: 'byField is not a reference field',
1258
- });
1259
- }
1260
- const { limit, page, skip, projection, sort } = parseQuery(req);
1261
- // policy on target read
1262
- const ctx = ctxFromReq(req);
1263
- const tPolicy = {
1264
- allowedMethods: targetSchema.allowedMethods,
1265
- access: targetSchema.access,
1266
- };
1267
- const tDec = (0, authorize_1.authorize)(targetLC, 'read', tPolicy, ctx);
1268
- if (!tDec.allow)
1269
- return res.status(403).json({ error: true, message: 'forbidden' });
1270
- // reverse filter (DB-agnostic; adapter maps appropriately)
1271
- const filter = rinfo.isArray
1272
- ? { [byField]: { $in: [id] } }
1273
- : { [byField]: id };
1274
- const adapterAny = this.dbAdapter;
1275
- let items = [];
1276
- let totalRows = 0;
1277
- if (typeof adapterAny.findMany === 'function') {
1278
- items = await adapterAny.findMany(targetSchema.modelName, filter, {
1279
- sort,
1280
- skip,
1281
- limit,
1282
- projection,
1283
- });
1284
- if (typeof adapterAny.count === 'function') {
1285
- totalRows = await adapterAny.count(targetSchema.modelName, filter);
1286
- }
1287
- else {
1288
- // fallback count
1289
- const all = await this.getModel(targetLC).read(filter, 0, 0, !!tDec.exposePrivate);
1290
- totalRows = all.length;
1291
- }
1292
- }
1293
- else {
1294
- // fallback (BaseModel.read); note: no projection/sort
1295
- const all = await this.getModel(targetLC).read(filter, 0, 0, !!tDec.exposePrivate);
1296
- totalRows = all.length;
1297
- items = all.slice(skip, skip + limit);
1298
- }
1299
- // If extended, hydrate with base data using baseId before masking
1300
- if (targetSchema.extends && Array.isArray(items) && items.length) {
1301
- const baseName = String(targetSchema.extends);
1302
- const baseLC = baseName.toLowerCase();
1303
- const baseModel = this.getModel(baseLC);
1304
- const baseIds = Array.from(new Set(items.map((it) => it?.baseId).filter(Boolean)));
1305
- if (baseIds.length) {
1306
- const baseDocs = await baseModel.read({ id: { $in: baseIds } }, baseIds.length, 0, !!tDec.exposePrivate);
1307
- const baseMap = new Map(baseDocs.map((b) => [String(b?.id ?? b?._id), b]));
1308
- items = items.map((it) => {
1309
- const bid = String(it?.baseId || '');
1310
- const b = bid ? baseMap.get(bid) : null;
1311
- const merged = b ? { ...b, ...it } : { ...it };
1312
- delete merged.baseId;
1313
- return merged;
1314
- });
1315
- }
1316
- else {
1317
- items = items.map((it) => {
1318
- const copy = { ...it };
1319
- delete copy.baseId;
1320
- return copy;
1321
- });
1322
- }
1323
- }
1324
- const masked = tDec.exposePrivate
1325
- ? (0, authorize_1.applyReadMaskMany)(items, tDec.sensitiveMask)
1326
- : (0, authorize_1.applyReadMaskMany)(items, tDec.readMask);
1327
- return res.status(200).json({
1328
- success: true,
1329
- message: `Data fetched for ${targetSchema.modelName}`,
1330
- data: masked,
1331
- pagination: { totalRows, page, limit },
1332
- });
1333
- }
1334
- catch (err) {
1335
- if (err?.code === 'MODEL_REMOVED') {
1336
- return res.status(410).json({ error: true, message: err.message });
1337
- }
1338
- return res
1339
- .status(400)
1340
- .json({ error: true, message: err?.message || 'Error' });
1341
- }
1342
- });
1343
- // keep 404 as last if needed
327
+ (0, mountFindRoutes_1.mountFindRoutes)(this.createCtx());
1344
328
  this.ensureNotFoundLast();
1345
329
  }
1346
- /** Mount generic file endpoints under this router */
1347
- setupFileRoutes() {
1348
- if (!this.fileStorage)
1349
- return;
1350
- const upload = (0, multer_1.default)({
1351
- storage: multer_1.default.memoryStorage(),
1352
- limits: { fileSize: 50 * 1024 * 1024, files: 20 }, // 50MB
1353
- });
1354
- // POST /files
1355
- this.router.post('/files', this.fileAuthMiddleware, upload.any(), async (req, res) => {
1356
- try {
1357
- const files = req.files ?? [];
1358
- if (!files.length) {
1359
- return res
1360
- .status(400)
1361
- .json({ error: true, message: 'No files uploaded' });
1362
- }
1363
- const results = await Promise.all(files.map(async (f) => {
1364
- const folder = this.shortFolder(); // uploads/YYYY/MM/DD
1365
- const ext = (f.originalname.match(/\.([A-Za-z0-9]{1,8})$/)?.[1] ??
1366
- (0, filename_1.extFromMime)(f.mimetype) ??
1367
- 'bin').toLowerCase();
1368
- const key = `${folder}/${this.shortUid()}.${ext}`;
1369
- const out = await this.fileStorage.upload({
1370
- key,
1371
- body: f.buffer,
1372
- contentType: f.mimetype,
1373
- metadata: { originalName: f.originalname || '' },
1374
- });
1375
- return {
1376
- provider: out.provider,
1377
- bucket: out.bucket,
1378
- key: out.key,
1379
- url: out.url,
1380
- etag: out.etag,
1381
- contentType: out.contentType,
1382
- size: out.size,
1383
- metadata: out.metadata,
1384
- originalName: f.originalname,
1385
- };
1386
- }));
1387
- return res.json({
1388
- success: true,
1389
- message: 'Files uploaded successfully',
1390
- data: results,
1391
- });
1392
- }
1393
- catch (err) {
1394
- return res
1395
- .status(400)
1396
- .json({ error: true, message: err?.message ?? 'Upload failed' });
1397
- }
1398
- });
1399
- // DELETE /files/:key(*)
1400
- this.router.delete('/files/:key(*)', this.fileAuthMiddleware, async (req, res) => {
1401
- try {
1402
- const key = String(req.params.key || '');
1403
- if (!key) {
1404
- return res
1405
- .status(400)
1406
- .json({ error: true, message: 'Key is required' });
1407
- }
1408
- const { deleted } = await this.fileStorage.delete(key);
1409
- return res.json({
1410
- success: true,
1411
- message: deleted ? 'File deleted' : 'Delete attempted',
1412
- key,
1413
- deleted,
1414
- });
1415
- }
1416
- catch (err) {
1417
- return res
1418
- .status(400)
1419
- .json({ error: true, message: err?.message ?? 'Delete failed' });
1420
- }
1421
- });
1422
- }
1423
- /** uploads/YYYY/MM/DD */
1424
- shortFolder() {
1425
- const d = new Date();
1426
- const y = d.getFullYear();
1427
- const m = String(d.getMonth() + 1).padStart(2, '0');
1428
- const day = String(d.getDate()).padStart(2, '0');
1429
- return `uploads/${y}/${m}/${day}`;
1430
- }
1431
- /** small, URL-safe uid */
1432
- shortUid() {
1433
- return (Date.now().toString(36) + Math.random().toString(36).slice(2, 6)).toLowerCase();
1434
- }
1435
- normalizeAttrType(attr) {
1436
- const a = Array.isArray(attr) ? attr?.[0] : attr;
1437
- let t = a?.type ?? a;
1438
- if (typeof t === 'function' && t.name)
1439
- t = t.name;
1440
- if (t && typeof t === 'object' && 'name' in t)
1441
- t = t.name;
1442
- if (typeof t === 'string')
1443
- t = t.toLowerCase();
1444
- if (t === 'bool')
1445
- t = 'boolean';
1446
- if (t === 'objectid' || t === 'oid' || t === 'ref')
1447
- t = 'objectid';
1448
- return String(t || '');
330
+ // ---------- Context builder for modules ----------
331
+ createCtx() {
332
+ return {
333
+ router: this.router,
334
+ dbAdapter: this.dbAdapter,
335
+ schemaLoader: this.schemaLoader,
336
+ liveSchemas: this.liveSchemas,
337
+ models: this.models,
338
+ jwtSecret: this.jwtSecret,
339
+ trustedApiKey: this.trustedApiKey,
340
+ fileStorage: this.fileStorage,
341
+ getSchema: this.getSchema,
342
+ getModel: this.getModel,
343
+ apiKeyMiddleware: this.apiKeyMiddleware,
344
+ optionalAuthMiddleware: this.optionalAuthMiddleware,
345
+ authenticateMiddleware: this.authenticateMiddleware,
346
+ pickAuthFor: this.pickAuthFor,
347
+ validateRequiredFields: this.validateRequiredFields,
348
+ handleWriteError: this.handleWriteError.bind(this),
349
+ checkUniqueFields: this.checkUniqueFields.bind(this),
350
+ normalizeRoleName: this.normalizeRoleName.bind(this),
351
+ };
1449
352
  }
1450
353
  pickAuthFor(action, modelNameLC) {
1451
354
  try {
@@ -1456,347 +359,35 @@ class APIRouter {
1456
359
  return this.optionalAuthMiddleware;
1457
360
  if (publicRule === false)
1458
361
  return this.authenticateMiddleware;
1459
- return action === 'read'
1460
- ? this.optionalAuthMiddleware
1461
- : this.authenticateMiddleware;
362
+ return action === "read" ? this.optionalAuthMiddleware : this.authenticateMiddleware;
1462
363
  }
1463
364
  catch {
1464
- return action === 'read'
1465
- ? this.optionalAuthMiddleware
1466
- : this.authenticateMiddleware;
365
+ return action === "read" ? this.optionalAuthMiddleware : this.authenticateMiddleware;
1467
366
  }
1468
367
  }
1469
368
  getUserRoleFromReq(req) {
1470
369
  const r = req.user?.role;
1471
- return typeof r === 'string'
1472
- ? r
1473
- : typeof r?.name === 'string'
1474
- ? r.name
1475
- : null;
1476
- }
1477
- setupAuthRoutes() {
1478
- const basePath = '/auth/users';
1479
- // ---- REGISTER ----
1480
- this.router.post(`${basePath}/register`, this.apiKeyMiddleware, async (req, res) => {
1481
- try {
1482
- let { email, username, password, ...rest } = req.body;
1483
- // Basic password policy (keep minimal, align with change-password)
1484
- if (String(password).length < 8) {
1485
- return res.status(400).json({
1486
- error: true,
1487
- message: 'Password must be at least 8 characters long',
1488
- });
1489
- }
1490
- const userModel = this.getModel('users');
1491
- const usersSchema = this.getSchema('users');
1492
- // Normalize unique identifiers
1493
- if (email)
1494
- email = String(email).trim().toLowerCase();
1495
- if (username)
1496
- username = String(username).trim();
1497
- // Prevent client from sending password in plain; we will hash below
1498
- const data = { ...rest };
1499
- if (email)
1500
- data.email = email;
1501
- if (username)
1502
- data.username = username;
1503
- // Provide a sensible default status so login works immediately
1504
- if (data.status == null)
1505
- data.status = 'active';
1506
- // Validate required fields against schema (create mode)
1507
- if (usersSchema) {
1508
- const missing = this.validateRequiredFields(usersSchema, {
1509
- ...data,
1510
- password,
1511
- });
1512
- if (missing.length > 0) {
1513
- return res.status(400).json({
1514
- error: true,
1515
- message: `Missing required field(s): ${missing.join(', ')}`,
1516
- });
1517
- }
1518
- // Check unique constraints before writing
1519
- const conflicts = await this.checkUniqueFields(usersSchema, data);
1520
- if (conflicts && conflicts.length) {
1521
- return res.status(400).json({
1522
- error: true,
1523
- message: `Duplicate value for field(s): ${conflicts.join(', ')}`,
1524
- });
1525
- }
1526
- }
1527
- // Hash password
1528
- const salt = await bcrypt_1.default.genSalt(10);
1529
- const hashed = await bcrypt_1.default.hash(String(password) + this.jwtSecret, salt);
1530
- data.password = hashed;
1531
- // Create user
1532
- const created = await userModel.create(data);
1533
- // Prepare response: generate token and strip password
1534
- const roleName = (await this.normalizeRoleName(created.role)) ?? '';
1535
- const token = jsonwebtoken_1.default.sign({ id: created.id, role: roleName }, this.jwtSecret, {
1536
- expiresIn: '7days',
1537
- });
1538
- delete created.password;
1539
- return res.json({
1540
- success: true,
1541
- message: 'Registration successful.',
1542
- data: { token, user: created },
1543
- });
1544
- }
1545
- catch (error) {
1546
- this.handleWriteError(error, res);
1547
- }
1548
- });
1549
- // ---- LOGIN ----
1550
- this.router.post(`${basePath}/login`, this.apiKeyMiddleware, async (req, res) => {
1551
- let { email, username, password } = req.body;
1552
- if ((!email && !username) || !password) {
1553
- return res.status(400).json({
1554
- error: true,
1555
- message: 'Email/username and password are required',
1556
- });
1557
- }
1558
- try {
1559
- const userModel = this.getModel('users');
1560
- const findBy = email
1561
- ? { email: String(email).trim().toLowerCase() }
1562
- : { username: String(username).trim() };
1563
- const users = await userModel.read(findBy, 1, 0, true);
1564
- const user = users?.[0];
1565
- if (!user)
1566
- return res
1567
- .status(400)
1568
- .json({ error: true, message: 'Invalid credentials' });
1569
- const hashedPassword = user.password;
1570
- if (!hashedPassword)
1571
- return res
1572
- .status(400)
1573
- .json({ error: true, message: 'User password not set' });
1574
- const isMatch = await bcrypt_1.default.compare(String(password) + this.jwtSecret, hashedPassword);
1575
- if (!isMatch)
1576
- return res
1577
- .status(400)
1578
- .json({ error: true, message: 'Invalid credentials' });
1579
- const status = user.status;
1580
- if (status && status !== 'active') {
1581
- const msg = status === 'pending'
1582
- ? 'Your account is awaiting approval.'
1583
- : 'Your account is suspended.';
1584
- return res.status(403).json({ error: true, message: msg });
1585
- }
1586
- const roleName = (await this.normalizeRoleName(user.role)) ?? '';
1587
- const token = jsonwebtoken_1.default.sign({ id: user.id, role: roleName }, // normalize here
1588
- this.jwtSecret, { expiresIn: '7days' });
1589
- delete user.password;
1590
- res.json({
1591
- success: true,
1592
- message: 'You are successfully logged in.',
1593
- data: { token, user },
1594
- });
1595
- }
1596
- catch (error) {
1597
- if (error?.code === 'MODEL_REMOVED')
1598
- return res
1599
- .status(410)
1600
- .json({ error: true, message: error.message });
1601
- res.status(500).json({ error: true, message: error.message });
1602
- }
1603
- });
1604
- // ---- ME ----
1605
- this.router.get(`${basePath}/me`, this.apiKeyMiddleware, this.authenticateMiddleware.bind(this), async (req, res) => {
1606
- try {
1607
- const userId = req.user?.id ?? req.user?._id ?? null;
1608
- if (!userId) {
1609
- return res
1610
- .status(401)
1611
- .json({ error: true, message: 'Not authenticated' });
1612
- }
1613
- const userModel = this.getModel('users');
1614
- const arr = await userModel.read({ id: userId }, 1, 0, true);
1615
- const user = arr?.[0];
1616
- if (!user) {
1617
- return res
1618
- .status(404)
1619
- .json({ error: true, message: 'User not found' });
1620
- }
1621
- delete user.password;
1622
- return res.json({ success: true, data: user });
1623
- }
1624
- catch (error) {
1625
- return res
1626
- .status(400)
1627
- .json({ error: true, message: error?.message || 'Error' });
1628
- }
1629
- });
1630
- // ---- CHANGE PASSWORD ----
1631
- this.router.post(`${basePath}/change-password`, this.apiKeyMiddleware, this.authenticateMiddleware.bind(this), async (req, res) => {
1632
- try {
1633
- const { oldPassword, newPassword } = req.body;
1634
- if (!oldPassword || !newPassword) {
1635
- return res.status(400).json({
1636
- error: true,
1637
- message: 'oldPassword and newPassword are required',
1638
- });
1639
- }
1640
- // basic policy (customize as desired)
1641
- if (String(newPassword).length < 8) {
1642
- return res.status(400).json({
1643
- error: true,
1644
- message: 'New password must be at least 8 characters long',
1645
- });
1646
- }
1647
- if (String(newPassword) === String(oldPassword)) {
1648
- return res.status(400).json({
1649
- error: true,
1650
- message: 'New password must be different from old password',
1651
- });
1652
- }
1653
- const userId = req.user?.id ?? req.user?._id ?? null;
1654
- if (!userId) {
1655
- return res
1656
- .status(401)
1657
- .json({ error: true, message: 'Not authenticated' });
1658
- }
1659
- const userModel = this.getModel('users');
1660
- const arr = await userModel.read({ id: userId }, 1, 0, true);
1661
- const user = arr?.[0];
1662
- if (!user) {
1663
- return res
1664
- .status(404)
1665
- .json({ error: true, message: 'User not found' });
1666
- }
1667
- const storedHash = user.password;
1668
- if (!storedHash) {
1669
- return res
1670
- .status(400)
1671
- .json({ error: true, message: 'User password not set' });
1672
- }
1673
- const match = await bcrypt_1.default.compare(String(oldPassword) + this.jwtSecret, storedHash);
1674
- if (!match) {
1675
- return res
1676
- .status(400)
1677
- .json({ error: true, message: 'Old password is incorrect' });
1678
- }
1679
- const salt = await bcrypt_1.default.genSalt(10);
1680
- const newHash = await bcrypt_1.default.hash(String(newPassword) + this.jwtSecret, salt);
1681
- await userModel.update(user.id, { password: newHash });
1682
- return res.json({
1683
- success: true,
1684
- message: 'Password changed successfully.',
1685
- });
1686
- }
1687
- catch (error) {
1688
- res
1689
- .status(400)
1690
- .json({ error: true, message: error?.message || 'Error' });
1691
- }
1692
- });
1693
- // ---- FORGOT PASSWORD ----
1694
- const forgotPasswordHandler = async (req, res) => {
1695
- try {
1696
- const { email } = (req.body || {});
1697
- if (!email || !String(email).trim()) {
1698
- return res
1699
- .status(400)
1700
- .json({ error: true, message: 'Email is required' });
1701
- }
1702
- // Normalize and (optionally) look up the user. We do NOT reveal whether the user exists.
1703
- const normalizedEmail = String(email).trim().toLowerCase();
1704
- try {
1705
- const userModel = this.getModel('users');
1706
- // Best-effort read; ignore result to avoid leaking existence
1707
- await userModel.read({ email: normalizedEmail }, 1, 0, true);
1708
- }
1709
- catch {
1710
- // Ignore lookup errors deliberately
1711
- }
1712
- // In a real implementation, generate a reset token, store it with expiry, and send an email.
1713
- // Here we return a generic success message so the React app flow can proceed.
1714
- return res.json({
1715
- success: true,
1716
- message: 'If an account exists, reset instructions have been sent.',
1717
- });
1718
- }
1719
- catch (error) {
1720
- return res
1721
- .status(400)
1722
- .json({ error: true, message: error?.message || 'Error' });
1723
- }
1724
- };
1725
- // Primary (plural) route under /auth/users
1726
- this.router.post(`${basePath}/forgot-password`, this.apiKeyMiddleware, forgotPasswordHandler);
1727
- // NOTE: All Users/Roles CRUD routes are handled by generic routes.
1728
- }
1729
- // ---------------- Common helpers ----------------
1730
- /**
1731
- * Validate required fields.
1732
- * - create: field must exist and be non-empty (not null/undefined/'').
1733
- * - update: only validate fields that are explicitly present in the payload;
1734
- * absence means "unchanged".
1735
- */
1736
- validateRequiredFields(schema, payload, mode = 'create') {
1737
- const missing = [];
1738
- const isEmpty = (v) => v === undefined ||
1739
- v === null ||
1740
- (typeof v === 'string' && v.trim() === '');
1741
- // If this is an extended schema, skip validating base fields here
1742
- let baseKeys = null;
1743
- const baseName = schema?.extends;
1744
- if (baseName) {
1745
- try {
1746
- const baseLC = baseName.toLowerCase();
1747
- const baseSchema = this.getSchema(baseLC);
1748
- baseKeys = new Set(Object.keys(baseSchema.attributes || {}));
1749
- }
1750
- catch {
1751
- baseKeys = null;
1752
- }
1753
- }
1754
- for (const [key, attribute] of Object.entries(schema.attributes)) {
1755
- // Skip array defs here (arrays wrap inner type), and skip private fields entirely from client-required validation
1756
- if (Array.isArray(attribute))
1757
- continue;
1758
- if (attribute?.private)
1759
- continue;
1760
- if (baseKeys && key !== 'baseId' && baseKeys.has(key))
1761
- continue; // do not require base fields on child
1762
- const req = Boolean(attribute?.required);
1763
- if (!req)
1764
- continue;
1765
- if (mode === 'create') {
1766
- if (isEmpty(payload[key]))
1767
- missing.push(key);
1768
- }
1769
- else {
1770
- // update → only if field is being sent AND is empty
1771
- if (Object.prototype.hasOwnProperty.call(payload, key) &&
1772
- isEmpty(payload[key])) {
1773
- missing.push(key);
1774
- }
1775
- }
1776
- }
1777
- return missing;
370
+ return typeof r === "string" ? r : typeof r?.name === "string" ? r.name : null;
1778
371
  }
1779
372
  handleWriteError(error, res) {
1780
- if (error?.code === 'MODEL_REMOVED') {
373
+ if (error?.code === "MODEL_REMOVED") {
1781
374
  res.status(410).json({ error: true, message: error.message });
1782
375
  return;
1783
376
  }
1784
377
  if (error?.code === 11000) {
1785
378
  const field = Object.keys(error.keyPattern || {})[0];
1786
- res
1787
- .status(400)
1788
- .json({ error: true, message: `Duplicate value for field: ${field}` });
379
+ res.status(400).json({ error: true, message: `Duplicate value for field: ${field}` });
1789
380
  }
1790
381
  else {
1791
- res.status(400).json({ error: true, message: error?.message || 'Error' });
382
+ res.status(400).json({ error: true, message: error?.message || "Error" });
1792
383
  }
1793
384
  }
1794
385
  setupNotFoundMiddleware() {
1795
386
  if (!this.notFoundHandler) {
1796
387
  this.notFoundHandler = (req, res) => {
1797
- Logger_1.default.warn('apiRouter', `API route not found: ${req.originalUrl}`);
388
+ Logger_1.default.warn("apiRouter", `API route not found: ${req.originalUrl}`);
1798
389
  res.status(404).json({
1799
- error: 'API route not found',
390
+ error: "API route not found",
1800
391
  path: req.originalUrl,
1801
392
  method: req.method,
1802
393
  });
@@ -1809,14 +400,7 @@ class APIRouter {
1809
400
  return;
1810
401
  // @ts-ignore
1811
402
  this.router.stack = this.router.stack.filter((layer) => layer?.handle !== this.notFoundHandler);
1812
- this.router.use('*', this.notFoundHandler);
1813
- }
1814
- async validateRoleValue(roleValue) {
1815
- const roleModel = this.models['role'];
1816
- if (!roleModel)
1817
- return false;
1818
- const roles = await roleModel.read({ name: roleValue }, 1, 0);
1819
- return roles.length > 0;
403
+ this.router.use("*", this.notFoundHandler);
1820
404
  }
1821
405
  async checkUniqueFields(schema, data, excludeId) {
1822
406
  const uniqueFields = Object.entries(schema.attributes)