@airoom/nextmin-node 0.1.5 → 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.
@@ -1,7 +1,7 @@
1
- import express from "express";
2
- import type { Server as HttpServer } from "http";
3
- import { DatabaseAdapter } from "../database/DatabaseAdapter";
4
- import type { FileStorageAdapter } from "../files/FileStorageAdapter";
1
+ import express from 'express';
2
+ import type { Server as HttpServer } from 'http';
3
+ import { DatabaseAdapter } from '../database/DatabaseAdapter';
4
+ import type { FileStorageAdapter } from '../files/FileStorageAdapter';
5
5
  export interface APIRouterOptions {
6
6
  dbAdapter: DatabaseAdapter;
7
7
  server?: HttpServer;
@@ -22,6 +22,7 @@ export declare class APIRouter {
22
22
  private liveSchemas;
23
23
  private notFoundHandler?;
24
24
  private fileStorage?;
25
+ private fileRoutesMounted;
25
26
  constructor(options: APIRouterOptions);
26
27
  getRouter(): express.Router;
27
28
  private wireSchemaHotReload;
@@ -32,6 +33,7 @@ export declare class APIRouter {
32
33
  private mountSchemasEndpointOnce;
33
34
  private mountRoutes;
34
35
  private mountFindRoutes;
36
+ private mountFileRoutes;
35
37
  private createCtx;
36
38
  private getSchema;
37
39
  private getModel;
@@ -12,6 +12,7 @@ const SchemaLoader_1 = require("../utils/SchemaLoader");
12
12
  const InMemoryAdapter_1 = require("../database/InMemoryAdapter");
13
13
  const DefaultDataInitializer_1 = require("../utils/DefaultDataInitializer");
14
14
  const SchemaService_1 = require("../services/SchemaService");
15
+ const setupFileRoutes_1 = require("./router/setupFileRoutes");
15
16
  const setupAuthRoutes_1 = require("./router/setupAuthRoutes");
16
17
  const mountCrudRoutes_1 = require("./router/mountCrudRoutes");
17
18
  const mountFindRoutes_1 = require("./router/mountFindRoutes");
@@ -23,12 +24,13 @@ class APIRouter {
23
24
  this.schemasRouteRegistered = false;
24
25
  this.registeredModels = new Set();
25
26
  this.liveSchemas = {};
27
+ this.fileRoutesMounted = false;
26
28
  // ---------- Live lookups ----------
27
29
  this.getSchema = (name) => {
28
30
  const s = this.liveSchemas[name];
29
31
  if (!s) {
30
32
  const err = new Error(`Model '${name}' removed`);
31
- err.code = "MODEL_REMOVED";
33
+ err.code = 'MODEL_REMOVED';
32
34
  throw err;
33
35
  }
34
36
  return s;
@@ -38,7 +40,7 @@ class APIRouter {
38
40
  const m = this.models[name];
39
41
  if (!m) {
40
42
  const err = new Error(`Model '${name}' unavailable`);
41
- err.code = "MODEL_UNAVAILABLE";
43
+ err.code = 'MODEL_UNAVAILABLE';
42
44
  throw err;
43
45
  }
44
46
  return m;
@@ -49,7 +51,7 @@ class APIRouter {
49
51
  const authHeader = req.headers.authorization;
50
52
  if (!authHeader)
51
53
  return next();
52
- const token = authHeader.split(" ")[1];
54
+ const token = authHeader.split(' ')[1];
53
55
  if (!token)
54
56
  return next();
55
57
  try {
@@ -62,7 +64,7 @@ class APIRouter {
62
64
  });
63
65
  };
64
66
  this.normalizeRoleName = async (value) => {
65
- const rolesModel = this.getModel("roles");
67
+ const rolesModel = this.getModel('roles');
66
68
  const isHex24 = (s) => /^[0-9a-fA-F]{24}$/.test(s);
67
69
  const fromString = async (s) => {
68
70
  if (!s)
@@ -70,17 +72,17 @@ class APIRouter {
70
72
  if (isHex24(s) && rolesModel) {
71
73
  const docs = await rolesModel.read({ id: s }, 1, 0, true);
72
74
  const n = docs?.[0]?.name;
73
- return typeof n === "string" ? n : null;
75
+ return typeof n === 'string' ? n : null;
74
76
  }
75
77
  return s;
76
78
  };
77
- if (typeof value === "string")
79
+ if (typeof value === 'string')
78
80
  return (await fromString(value))?.toLowerCase() ?? null;
79
81
  if (Array.isArray(value)) {
80
82
  for (const v of value) {
81
- const n = typeof v === "string"
83
+ const n = typeof v === 'string'
82
84
  ? await fromString(v)
83
- : v && typeof v === "object" && typeof v.name === "string"
85
+ : v && typeof v === 'object' && typeof v.name === 'string'
84
86
  ? v.name
85
87
  : null;
86
88
  if (n)
@@ -88,11 +90,11 @@ class APIRouter {
88
90
  }
89
91
  return null;
90
92
  }
91
- if (value && typeof value === "object") {
93
+ if (value && typeof value === 'object') {
92
94
  const o = value;
93
- if (typeof o.name === "string")
95
+ if (typeof o.name === 'string')
94
96
  return o.name.toLowerCase();
95
- if (typeof o.id === "string") {
97
+ if (typeof o.id === 'string') {
96
98
  const n = await fromString(o.id);
97
99
  return n ? n.toLowerCase() : null;
98
100
  }
@@ -101,9 +103,9 @@ class APIRouter {
101
103
  };
102
104
  // ---------- auth middlewares ----------
103
105
  this.apiKeyMiddleware = (req, res, next) => {
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" });
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' });
107
109
  }
108
110
  next();
109
111
  };
@@ -111,10 +113,10 @@ class APIRouter {
111
113
  this.apiKeyMiddleware(req, res, () => {
112
114
  const authHeader = req.headers.authorization;
113
115
  if (!authHeader)
114
- return res.status(401).json({ error: "Authorization header missing" });
115
- const token = authHeader.split(" ")[1];
116
+ return res.status(401).json({ error: 'Authorization header missing' });
117
+ const token = authHeader.split(' ')[1];
116
118
  if (!token)
117
- return res.status(401).json({ error: "API Token missing." });
119
+ return res.status(401).json({ error: 'API Token missing.' });
118
120
  try {
119
121
  const decoded = jsonwebtoken_1.default.verify(token, this.jwtSecret);
120
122
  // @ts-ignore
@@ -122,14 +124,16 @@ class APIRouter {
122
124
  next();
123
125
  }
124
126
  catch {
125
- res.status(401).json({ error: "Invalid or expired token" });
127
+ res.status(401).json({ error: 'Invalid or expired token' });
126
128
  }
127
129
  });
128
130
  };
129
131
  // ---------- helpers ----------
130
- this.validateRequiredFields = (schema, payload, mode = "create") => {
132
+ this.validateRequiredFields = (schema, payload, mode = 'create') => {
131
133
  const missing = [];
132
- const isEmpty = (v) => v === undefined || v === null || (typeof v === "string" && v.trim() === "");
134
+ const isEmpty = (v) => v === undefined ||
135
+ v === null ||
136
+ (typeof v === 'string' && v.trim() === '');
133
137
  let baseKeys = null;
134
138
  const baseName = schema?.extends;
135
139
  if (baseName) {
@@ -147,76 +151,77 @@ class APIRouter {
147
151
  continue;
148
152
  if (attribute?.private)
149
153
  continue;
150
- if (baseKeys && key !== "baseId" && baseKeys.has(key))
154
+ if (baseKeys && key !== 'baseId' && baseKeys.has(key))
151
155
  continue;
152
156
  const reqd = Boolean(attribute?.required);
153
157
  if (!reqd)
154
158
  continue;
155
- if (mode === "create") {
159
+ if (mode === 'create') {
156
160
  if (isEmpty(payload[key]))
157
161
  missing.push(key);
158
162
  }
159
163
  else {
160
- if (Object.prototype.hasOwnProperty.call(payload, key) && isEmpty(payload[key])) {
164
+ if (Object.prototype.hasOwnProperty.call(payload, key) &&
165
+ isEmpty(payload[key])) {
161
166
  missing.push(key);
162
167
  }
163
168
  }
164
169
  }
165
170
  return missing;
166
171
  };
167
- this.isDevelopment = process.env.APP_MODE !== "production";
172
+ this.isDevelopment = process.env.APP_MODE !== 'production';
168
173
  this.router = express_1.default.Router();
169
174
  this.fileStorage = options.fileStorageAdapter;
170
175
  this.dbAdapter = options.dbAdapter || new InMemoryAdapter_1.InMemoryAdapter();
171
- this.jwtSecret = process.env.JWT_SECRET || "default_jwt_secret";
176
+ this.jwtSecret = process.env.JWT_SECRET || 'default_jwt_secret';
172
177
  if (this.isDevelopment && options.server) {
173
178
  (0, SchemaService_1.startSchemaService)(options.server, {
174
179
  getApiKey: () => this.trustedApiKey,
175
180
  });
176
- options.server.on("listening", () => {
181
+ options.server.on('listening', () => {
177
182
  // @ts-ignore
178
183
  const addr = options.server.address();
179
- Logger_1.default.info("SchemaService", `[schema-service] started at /__nextmin__/schema ns /schema on ${typeof addr === "string" ? addr : `${addr?.address}:${addr?.port}`}`);
184
+ Logger_1.default.info('SchemaService', `[schema-service] started at /__nextmin__/schema ns /schema on ${typeof addr === 'string' ? addr : `${addr?.address}:${addr?.port}`}`);
180
185
  });
181
186
  }
182
- this.schemaLoader = SchemaLoader_1.SchemaLoader.getInstance?.() ?? new SchemaLoader_1.SchemaLoader();
187
+ this.schemaLoader =
188
+ SchemaLoader_1.SchemaLoader.getInstance?.() ?? new SchemaLoader_1.SchemaLoader();
183
189
  const initialSchemas = this.schemaLoader.getSchemas();
184
190
  this.setLiveSchemas(initialSchemas);
185
191
  const finishBoot = async () => {
186
- if (typeof this.dbAdapter.registerSchemas === "function") {
192
+ if (typeof this.dbAdapter.registerSchemas === 'function') {
187
193
  await this.dbAdapter.registerSchemas(initialSchemas);
188
194
  }
189
195
  this.rebuildModels(Object.values(initialSchemas));
190
196
  this.mountSchemasEndpointOnce();
191
197
  this.mountRoutes(initialSchemas);
192
198
  this.mountFindRoutes();
199
+ this.mountFileRoutes();
193
200
  await this.syncAllIndexes(initialSchemas);
194
201
  const initializer = new DefaultDataInitializer_1.DefaultDataInitializer(this.dbAdapter, this.models);
195
202
  try {
196
203
  await initializer.initialize();
197
- this.trustedApiKey = initializer.getApiKey() || "";
198
- Logger_1.default.info("APIRouter", `Trusted API key set: ${this.trustedApiKey ? "[hidden]" : "none"}`);
204
+ this.trustedApiKey = initializer.getApiKey() || '';
205
+ Logger_1.default.info('APIRouter', `Trusted API key set: ${this.trustedApiKey ? '[hidden]' : 'none'}`);
199
206
  }
200
207
  catch (err) {
201
- Logger_1.default.error("APIRouter", "Failed to initialize default data", err);
208
+ Logger_1.default.error('APIRouter', 'Failed to initialize default data', err);
202
209
  }
203
210
  this.setupNotFoundMiddleware();
204
211
  this.wireSchemaHotReload();
205
212
  };
206
- if (typeof this.dbAdapter.registerSchemas === "function") {
213
+ if (typeof this.dbAdapter.registerSchemas === 'function') {
207
214
  const res = this.dbAdapter.registerSchemas(initialSchemas);
208
215
  if (res instanceof Promise) {
209
- res
210
- .then(finishBoot)
211
- .catch((err) => {
212
- Logger_1.default.error("APIRouter", "registerSchemas failed", err);
216
+ res.then(finishBoot).catch((err) => {
217
+ Logger_1.default.error('APIRouter', 'registerSchemas failed', err);
213
218
  this.setupNotFoundMiddleware();
214
219
  });
215
220
  return;
216
221
  }
217
222
  }
218
223
  finishBoot().catch((err) => {
219
- Logger_1.default.error("APIRouter", "finishBoot error", err);
224
+ Logger_1.default.error('APIRouter', 'finishBoot error', err);
220
225
  this.setupNotFoundMiddleware();
221
226
  });
222
227
  }
@@ -226,15 +231,17 @@ class APIRouter {
226
231
  // ---------- Hot reload ----------
227
232
  wireSchemaHotReload() {
228
233
  const anyLoader = this.schemaLoader;
229
- if (typeof anyLoader.on !== "function")
234
+ if (typeof anyLoader.on !== 'function')
230
235
  return;
231
- anyLoader.on("schemasChanged", async (newSchemas) => {
236
+ anyLoader.on('schemasChanged', async (newSchemas) => {
232
237
  try {
233
238
  const removed = this.diffRemovedModels(this.liveSchemas, newSchemas);
234
- if (removed.length && typeof this.dbAdapter.unregisterSchemas === "function") {
239
+ if (removed.length &&
240
+ typeof this.dbAdapter.unregisterSchemas === 'function') {
235
241
  await this.dbAdapter.unregisterSchemas(removed);
236
242
  }
237
- else if (removed.length && typeof this.dbAdapter.dropModel === "function") {
243
+ else if (removed.length &&
244
+ typeof this.dbAdapter.dropModel === 'function') {
238
245
  for (const name of removed) {
239
246
  try {
240
247
  await this.dbAdapter.dropModel(name);
@@ -242,22 +249,22 @@ class APIRouter {
242
249
  catch { }
243
250
  }
244
251
  }
245
- if (typeof this.dbAdapter.registerSchemas === "function") {
252
+ if (typeof this.dbAdapter.registerSchemas === 'function') {
246
253
  await this.dbAdapter.registerSchemas(newSchemas);
247
254
  }
248
255
  this.setLiveSchemas(newSchemas);
249
256
  this.rebuildModels(Object.values(newSchemas));
250
257
  this.mountRoutes(newSchemas);
251
258
  await this.syncAllIndexes(newSchemas);
252
- Logger_1.default.info("APIRouter", `Schemas reloaded (added/updated: ${Object.keys(newSchemas).length}, removed: ${removed.join(", ") || "none"})`);
259
+ Logger_1.default.info('APIRouter', `Schemas reloaded (added/updated: ${Object.keys(newSchemas).length}, removed: ${removed.join(', ') || 'none'})`);
253
260
  }
254
261
  catch (err) {
255
- Logger_1.default.error("APIRouter", "Failed to refresh after schemasChanged", err);
262
+ Logger_1.default.error('APIRouter', 'Failed to refresh after schemasChanged', err);
256
263
  }
257
264
  });
258
265
  }
259
266
  async syncAllIndexes(schemas) {
260
- if (typeof this.dbAdapter.syncIndexes !== "function")
267
+ if (typeof this.dbAdapter.syncIndexes !== 'function')
261
268
  return;
262
269
  const plan = this.schemaLoader.getIndexPlan();
263
270
  for (const s of Object.values(schemas)) {
@@ -268,7 +275,7 @@ class APIRouter {
268
275
  await this.dbAdapter.syncIndexes(modelName, spec);
269
276
  }
270
277
  catch (err) {
271
- Logger_1.default.warn("APIRouter", `Index sync failed for ${modelName}: ${err?.message || err}`);
278
+ Logger_1.default.warn('APIRouter', `Index sync failed for ${modelName}: ${err?.message || err}`);
272
279
  }
273
280
  }
274
281
  }
@@ -280,7 +287,7 @@ class APIRouter {
280
287
  if (!next.has(name.toLowerCase()))
281
288
  removed.push(name.toLowerCase());
282
289
  }
283
- return removed.filter((n) => n !== "users" && n !== "roles");
290
+ return removed.filter((n) => n !== 'users' && n !== 'roles');
284
291
  }
285
292
  setLiveSchemas(schemas) {
286
293
  const idx = {};
@@ -299,7 +306,7 @@ class APIRouter {
299
306
  if (this.schemasRouteRegistered)
300
307
  return;
301
308
  this.schemasRouteRegistered = true;
302
- this.router.get("/_schemas", this.apiKeyMiddleware, (_req, res) => {
309
+ this.router.get('/_schemas', this.apiKeyMiddleware, (_req, res) => {
303
310
  res.json({
304
311
  success: true,
305
312
  data: this.schemaLoader.getPublicSchemaList(),
@@ -307,7 +314,7 @@ class APIRouter {
307
314
  });
308
315
  }
309
316
  mountRoutes(schemas) {
310
- if (!this.authRoutesInitialized && this.liveSchemas["users"]) {
317
+ if (!this.authRoutesInitialized && this.liveSchemas['users']) {
311
318
  (0, setupAuthRoutes_1.setupAuthRoutes)(this.createCtx());
312
319
  this.authRoutesInitialized = true;
313
320
  }
@@ -327,6 +334,15 @@ class APIRouter {
327
334
  (0, mountFindRoutes_1.mountFindRoutes)(this.createCtx());
328
335
  this.ensureNotFoundLast();
329
336
  }
337
+ mountFileRoutes() {
338
+ if (this.fileRoutesMounted)
339
+ return;
340
+ if (this.fileStorage) {
341
+ (0, setupFileRoutes_1.setupFileRoutes)(this.createCtx());
342
+ this.fileRoutesMounted = true;
343
+ this.ensureNotFoundLast();
344
+ }
345
+ }
330
346
  // ---------- Context builder for modules ----------
331
347
  createCtx() {
332
348
  return {
@@ -359,35 +375,45 @@ class APIRouter {
359
375
  return this.optionalAuthMiddleware;
360
376
  if (publicRule === false)
361
377
  return this.authenticateMiddleware;
362
- return action === "read" ? this.optionalAuthMiddleware : this.authenticateMiddleware;
378
+ return action === 'read'
379
+ ? this.optionalAuthMiddleware
380
+ : this.authenticateMiddleware;
363
381
  }
364
382
  catch {
365
- return action === "read" ? this.optionalAuthMiddleware : this.authenticateMiddleware;
383
+ return action === 'read'
384
+ ? this.optionalAuthMiddleware
385
+ : this.authenticateMiddleware;
366
386
  }
367
387
  }
368
388
  getUserRoleFromReq(req) {
369
389
  const r = req.user?.role;
370
- return typeof r === "string" ? r : typeof r?.name === "string" ? r.name : null;
390
+ return typeof r === 'string'
391
+ ? r
392
+ : typeof r?.name === 'string'
393
+ ? r.name
394
+ : null;
371
395
  }
372
396
  handleWriteError(error, res) {
373
- if (error?.code === "MODEL_REMOVED") {
397
+ if (error?.code === 'MODEL_REMOVED') {
374
398
  res.status(410).json({ error: true, message: error.message });
375
399
  return;
376
400
  }
377
401
  if (error?.code === 11000) {
378
402
  const field = Object.keys(error.keyPattern || {})[0];
379
- res.status(400).json({ error: true, message: `Duplicate value for field: ${field}` });
403
+ res
404
+ .status(400)
405
+ .json({ error: true, message: `Duplicate value for field: ${field}` });
380
406
  }
381
407
  else {
382
- res.status(400).json({ error: true, message: error?.message || "Error" });
408
+ res.status(400).json({ error: true, message: error?.message || 'Error' });
383
409
  }
384
410
  }
385
411
  setupNotFoundMiddleware() {
386
412
  if (!this.notFoundHandler) {
387
413
  this.notFoundHandler = (req, res) => {
388
- Logger_1.default.warn("apiRouter", `API route not found: ${req.originalUrl}`);
414
+ Logger_1.default.warn('apiRouter', `API route not found: ${req.originalUrl}`);
389
415
  res.status(404).json({
390
- error: "API route not found",
416
+ error: 'API route not found',
391
417
  path: req.originalUrl,
392
418
  method: req.method,
393
419
  });
@@ -400,7 +426,7 @@ class APIRouter {
400
426
  return;
401
427
  // @ts-ignore
402
428
  this.router.stack = this.router.stack.filter((layer) => layer?.handle !== this.notFoundHandler);
403
- this.router.use("*", this.notFoundHandler);
429
+ this.router.use('*', this.notFoundHandler);
404
430
  }
405
431
  async checkUniqueFields(schema, data, excludeId) {
406
432
  const uniqueFields = Object.entries(schema.attributes)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@airoom/nextmin-node",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",