@airoom/nextmin-node 1.4.6 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -5
- package/dist/api/apiRouter.d.ts +10 -1
- package/dist/api/apiRouter.js +83 -22
- package/dist/api/router/mountBatchRoutes.d.ts +2 -0
- package/dist/api/router/mountBatchRoutes.js +90 -0
- package/dist/api/router/mountCrudRoutes.js +218 -223
- package/dist/api/router/mountFindRoutes.js +2 -49
- package/dist/api/router/mountSearchRoutes.js +10 -52
- package/dist/api/router/mountSearchRoutes_extended.js +7 -48
- package/dist/api/router/setupFileRoutes.js +41 -0
- package/dist/api/router/utils.d.ts +2 -0
- package/dist/api/router/utils.js +37 -8
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +83 -0
- package/dist/database/DatabaseAdapter.d.ts +8 -0
- package/dist/database/NMAdapter.d.ts +46 -0
- package/dist/database/NMAdapter.js +1243 -0
- package/dist/database/QueryEngine.d.ts +14 -0
- package/dist/database/QueryEngine.js +215 -0
- package/dist/database/utils.d.ts +2 -0
- package/dist/database/utils.js +21 -0
- package/dist/files/FileStorageAdapter.d.ts +5 -0
- package/dist/files/LocalFileStorageAdapter.d.ts +5 -0
- package/dist/files/LocalFileStorageAdapter.js +14 -0
- package/dist/files/S3FileStorageAdapter.d.ts +5 -0
- package/dist/files/S3FileStorageAdapter.js +14 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +11 -5
- package/dist/models/BaseModel.d.ts +31 -2
- package/dist/models/BaseModel.js +33 -5
- package/dist/policy/authorize.js +95 -38
- package/dist/schemas/Users.json +66 -30
- package/dist/services/AggregateService.d.ts +8 -0
- package/dist/services/AggregateService.js +87 -0
- package/dist/services/RealtimeService.d.ts +20 -0
- package/dist/services/RealtimeService.js +93 -0
- package/dist/services/SchemaService.d.ts +3 -0
- package/dist/services/SchemaService.js +6 -2
- package/dist/utils/DefaultDataInitializer.js +10 -2
- package/dist/utils/Events.d.ts +34 -0
- package/dist/utils/Events.js +55 -0
- package/dist/utils/Logger.d.ts +2 -1
- package/dist/utils/Logger.js +20 -9
- package/dist/utils/QueryCache.d.ts +16 -0
- package/dist/utils/QueryCache.js +106 -0
- package/dist/utils/SchemaLoader.d.ts +6 -1
- package/dist/utils/SchemaLoader.js +49 -5
- package/package.json +19 -4
- package/dist/database/InMemoryAdapter.d.ts +0 -15
- package/dist/database/InMemoryAdapter.js +0 -71
- package/dist/database/MongoAdapter.d.ts +0 -52
- package/dist/database/MongoAdapter.js +0 -410
package/README.md
CHANGED
|
@@ -12,13 +12,15 @@ Read the full documentation at: https://nextmin.gscodes.dev/
|
|
|
12
12
|
## Highlights
|
|
13
13
|
|
|
14
14
|
- Express router factory: mount a complete REST API in a few lines
|
|
15
|
+
- **Native Realtime Support**: Built-in Socket.io integration for instant schema updates and data synchronization
|
|
16
|
+
- **Event-Driven Architecture**: Lifecycle hooks (`before:create`, `after:update`, etc.) for custom business logic
|
|
15
17
|
- Auth built in: register, login, me, change‑password, forgot‑password
|
|
16
18
|
- CRUD per model with read masks, write restrictions, and role/owner policies
|
|
17
19
|
- Advanced list endpoint: filter, multi‑field search, date ranges, multi‑field sort, paginate
|
|
18
20
|
- Relationship endpoints: forward and reverse lookups without autopopulate
|
|
19
21
|
- Schemas hot‑reload during development; automatic model wiring
|
|
20
22
|
- File uploads via pluggable storage (e.g., S3/MinIO); delete by key
|
|
21
|
-
- Database adapters:
|
|
23
|
+
- Database adapters: **NMAdapter (Recommended)** supports SQL (Postgres/SQLite/MySQL) and MongoDB via TypeORM. The standalone `MongoAdapter` and `InMemoryAdapter` are now deprecated.
|
|
22
24
|
- Emits a trusted API key stored in your Settings model for client access
|
|
23
25
|
|
|
24
26
|
## Installation
|
|
@@ -42,7 +44,7 @@ import express from 'express';
|
|
|
42
44
|
import http from 'http';
|
|
43
45
|
import {
|
|
44
46
|
createNextMinRouter,
|
|
45
|
-
|
|
47
|
+
NMAdapter,
|
|
46
48
|
S3FileStorageAdapter,
|
|
47
49
|
} from '@airoom/nextmin-node';
|
|
48
50
|
import cors from 'cors';
|
|
@@ -55,8 +57,12 @@ async function start() {
|
|
|
55
57
|
|
|
56
58
|
app.use(cors());
|
|
57
59
|
|
|
58
|
-
// 1) Database
|
|
59
|
-
const db = new
|
|
60
|
+
// 1) Database (NMAdapter supports SQL and MongoDB)
|
|
61
|
+
const db = new NMAdapter({
|
|
62
|
+
type: 'postgres', // or 'mongodb', 'sqlite', 'mysql'
|
|
63
|
+
url: process.env.DATABASE_URL,
|
|
64
|
+
synchronize: true, // typical for development
|
|
65
|
+
});
|
|
60
66
|
await db.connect();
|
|
61
67
|
|
|
62
68
|
// 2) Optional: file storage adapter (S3/MinIO)
|
|
@@ -77,7 +83,12 @@ async function start() {
|
|
|
77
83
|
});
|
|
78
84
|
|
|
79
85
|
// 3) Mount NextMin REST router
|
|
80
|
-
|
|
86
|
+
// Pass the 'server' instance to enable native Realtime/WebSockets
|
|
87
|
+
const router = createNextMinRouter({
|
|
88
|
+
dbAdapter: db,
|
|
89
|
+
server: server, // REQUIRED for realtime
|
|
90
|
+
fileStorageAdapter: files
|
|
91
|
+
});
|
|
81
92
|
app.use('/rest', router);
|
|
82
93
|
|
|
83
94
|
// 4) Listen with the same server instance
|
|
@@ -115,6 +126,38 @@ Base: `/rest`
|
|
|
115
126
|
- Upload: `POST /files` (multipart form, fields named `file`)
|
|
116
127
|
- Delete: `DELETE /files/:key(*)`
|
|
117
128
|
|
|
129
|
+
## Event System (Hooks)
|
|
130
|
+
|
|
131
|
+
NextMin provides a powerful event-driven system to inject custom logic before or after database operations.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { events, Events, getModelEvent } from '@airoom/nextmin-node';
|
|
135
|
+
|
|
136
|
+
// Global hook: Run logic after any document is created
|
|
137
|
+
events.on(Events.AFTER_CREATE, ({ modelName, data }) => {
|
|
138
|
+
console.log(`Document created in ${modelName}:`, data.id);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Model-specific hook: Send email after a User registers
|
|
142
|
+
events.on(getModelEvent('User', 'create', 'after'), ({ data }) => {
|
|
143
|
+
sendWelcomeEmail(data.email);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Validation hook: Prevent deletion if certain conditions aren't met
|
|
147
|
+
events.on(getModelEvent('Post', 'delete', 'before'), ({ id }) => {
|
|
148
|
+
if (isSystemProtected(id)) {
|
|
149
|
+
throw new Error("This post cannot be deleted.");
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Available Events
|
|
155
|
+
- `before:doc:create`, `after:doc:create`
|
|
156
|
+
- `before:doc:update`, `after:doc:update`
|
|
157
|
+
- `before:doc:delete`, `after:doc:delete`
|
|
158
|
+
- `before:doc:read`, `after:doc:read`
|
|
159
|
+
- `auth:login`, `auth:signup`, `schema:update`
|
|
160
|
+
|
|
118
161
|
See full examples in documentation and the `examples/node` app inside this monorepo.
|
|
119
162
|
|
|
120
163
|
## Headers and auth
|
package/dist/api/apiRouter.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export interface APIRouterOptions {
|
|
|
6
6
|
dbAdapter: DatabaseAdapter;
|
|
7
7
|
server?: HttpServer;
|
|
8
8
|
fileStorageAdapter?: FileStorageAdapter;
|
|
9
|
+
imageProxy?: {
|
|
10
|
+
originalBase?: string;
|
|
11
|
+
proxyBase?: string;
|
|
12
|
+
};
|
|
9
13
|
}
|
|
10
14
|
export declare class APIRouter {
|
|
11
15
|
private router;
|
|
@@ -14,16 +18,19 @@ export declare class APIRouter {
|
|
|
14
18
|
private jwtSecret;
|
|
15
19
|
private trustedApiKey;
|
|
16
20
|
private schemaLoader;
|
|
17
|
-
private isDevelopment;
|
|
21
|
+
private get isDevelopment();
|
|
18
22
|
private findRoutesMounted;
|
|
19
23
|
private searchRoutesMounted;
|
|
24
|
+
private batchRoutesMounted;
|
|
20
25
|
private authRoutesInitialized;
|
|
21
26
|
private schemasRouteRegistered;
|
|
22
27
|
private registeredModels;
|
|
23
28
|
private liveSchemas;
|
|
24
29
|
private notFoundHandler?;
|
|
25
30
|
private fileStorage?;
|
|
31
|
+
private realtimeService?;
|
|
26
32
|
private fileRoutesMounted;
|
|
33
|
+
private aggregateService?;
|
|
27
34
|
constructor(options: APIRouterOptions);
|
|
28
35
|
getRouter(): express.Router;
|
|
29
36
|
private wireSchemaHotReload;
|
|
@@ -32,9 +39,11 @@ export declare class APIRouter {
|
|
|
32
39
|
private setLiveSchemas;
|
|
33
40
|
private rebuildModels;
|
|
34
41
|
private mountSchemasEndpointOnce;
|
|
42
|
+
private mountCleanupEndpointOnce;
|
|
35
43
|
private mountRoutes;
|
|
36
44
|
private mountFindRoutes;
|
|
37
45
|
private mountSearchRoutes;
|
|
46
|
+
private mountBatchRoutes;
|
|
38
47
|
private mountFileRoutes;
|
|
39
48
|
private createCtx;
|
|
40
49
|
private getSchema;
|
package/dist/api/apiRouter.js
CHANGED
|
@@ -9,19 +9,26 @@ const BaseModel_1 = require("../models/BaseModel");
|
|
|
9
9
|
const Logger_1 = __importDefault(require("../utils/Logger"));
|
|
10
10
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
11
11
|
const SchemaLoader_1 = require("../utils/SchemaLoader");
|
|
12
|
-
const InMemoryAdapter_1 = require("../database/InMemoryAdapter");
|
|
13
12
|
const DefaultDataInitializer_1 = require("../utils/DefaultDataInitializer");
|
|
14
|
-
const
|
|
13
|
+
const RealtimeService_1 = require("../services/RealtimeService");
|
|
14
|
+
const Events_1 = require("../utils/Events");
|
|
15
|
+
const AggregateService_1 = require("../services/AggregateService");
|
|
15
16
|
const setupFileRoutes_1 = require("./router/setupFileRoutes");
|
|
16
17
|
const setupAuthRoutes_1 = require("./router/setupAuthRoutes");
|
|
17
18
|
const mountCrudRoutes_1 = require("./router/mountCrudRoutes");
|
|
18
19
|
const mountFindRoutes_1 = require("./router/mountFindRoutes");
|
|
19
20
|
const mountSearchRoutes_1 = require("./router/mountSearchRoutes");
|
|
21
|
+
const mountBatchRoutes_1 = require("./router/mountBatchRoutes");
|
|
20
22
|
class APIRouter {
|
|
23
|
+
get isDevelopment() {
|
|
24
|
+
return (process.env.APP_MODE?.toLowerCase() !== 'production' &&
|
|
25
|
+
process.env.NODE_ENV?.toLowerCase() !== 'production');
|
|
26
|
+
}
|
|
21
27
|
constructor(options) {
|
|
22
28
|
this.models = {};
|
|
23
29
|
this.findRoutesMounted = false;
|
|
24
30
|
this.searchRoutesMounted = false;
|
|
31
|
+
this.batchRoutesMounted = false;
|
|
25
32
|
this.authRoutesInitialized = false;
|
|
26
33
|
this.schemasRouteRegistered = false;
|
|
27
34
|
this.registeredModels = new Set();
|
|
@@ -101,6 +108,17 @@ class APIRouter {
|
|
|
101
108
|
return n ? n.toLowerCase() : null;
|
|
102
109
|
}
|
|
103
110
|
}
|
|
111
|
+
// Handle numeric IDs (common in SQL databases like SQLite)
|
|
112
|
+
if (typeof value === 'number' && rolesModel) {
|
|
113
|
+
try {
|
|
114
|
+
const docs = await rolesModel.read({ id: value }, 1, 0, true);
|
|
115
|
+
const n = docs?.[0]?.name;
|
|
116
|
+
return typeof n === 'string' ? n.toLowerCase() : null;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
104
122
|
return null;
|
|
105
123
|
};
|
|
106
124
|
// ---------- auth middlewares ----------
|
|
@@ -171,19 +189,18 @@ class APIRouter {
|
|
|
171
189
|
}
|
|
172
190
|
return missing;
|
|
173
191
|
};
|
|
174
|
-
this.isDevelopment = process.env.APP_MODE !== 'production';
|
|
175
192
|
this.router = express_1.default.Router();
|
|
176
193
|
this.fileStorage = options.fileStorageAdapter;
|
|
177
|
-
this.dbAdapter = options.dbAdapter
|
|
194
|
+
this.dbAdapter = options.dbAdapter;
|
|
178
195
|
this.jwtSecret = process.env.JWT_SECRET || 'default_jwt_secret';
|
|
179
|
-
if (
|
|
180
|
-
|
|
196
|
+
if (options.server) {
|
|
197
|
+
this.realtimeService = new RealtimeService_1.RealtimeService(options.server, {
|
|
181
198
|
getApiKey: () => this.trustedApiKey,
|
|
182
199
|
});
|
|
183
200
|
options.server.on('listening', () => {
|
|
184
201
|
// @ts-ignore
|
|
185
202
|
const addr = options.server.address();
|
|
186
|
-
Logger_1.default.info('
|
|
203
|
+
Logger_1.default.info('RealtimeService', `Started at /__nextmin__/realtime ns /realtime on ${typeof addr === 'string' ? addr : `${addr?.address}:${addr?.port}`}`);
|
|
187
204
|
});
|
|
188
205
|
}
|
|
189
206
|
this.schemaLoader =
|
|
@@ -196,9 +213,11 @@ class APIRouter {
|
|
|
196
213
|
}
|
|
197
214
|
this.rebuildModels(Object.values(initialSchemas));
|
|
198
215
|
this.mountSchemasEndpointOnce();
|
|
216
|
+
this.mountCleanupEndpointOnce();
|
|
199
217
|
this.mountRoutes(initialSchemas);
|
|
200
218
|
this.mountFindRoutes();
|
|
201
219
|
this.mountSearchRoutes();
|
|
220
|
+
this.mountBatchRoutes();
|
|
202
221
|
this.mountFileRoutes();
|
|
203
222
|
await this.syncAllIndexes(initialSchemas);
|
|
204
223
|
const initializer = new DefaultDataInitializer_1.DefaultDataInitializer(this.dbAdapter, this.models);
|
|
@@ -206,11 +225,13 @@ class APIRouter {
|
|
|
206
225
|
await initializer.initialize();
|
|
207
226
|
this.trustedApiKey = initializer.getApiKey() || '';
|
|
208
227
|
Logger_1.default.info('APIRouter', `Trusted API key set: ${this.trustedApiKey ? '[hidden]' : 'none'}`);
|
|
228
|
+
Events_1.events.emitEvent(Events_1.Events.SERVER_START, { trustedApiKey: this.trustedApiKey });
|
|
209
229
|
}
|
|
210
230
|
catch (err) {
|
|
211
231
|
Logger_1.default.error('APIRouter', 'Failed to initialize default data', err);
|
|
212
232
|
}
|
|
213
233
|
this.setupNotFoundMiddleware();
|
|
234
|
+
this.aggregateService = new AggregateService_1.AggregateService(this.createCtx());
|
|
214
235
|
this.wireSchemaHotReload();
|
|
215
236
|
};
|
|
216
237
|
if (typeof this.dbAdapter.registerSchemas === 'function') {
|
|
@@ -260,6 +281,7 @@ class APIRouter {
|
|
|
260
281
|
this.mountRoutes(newSchemas);
|
|
261
282
|
await this.syncAllIndexes(newSchemas);
|
|
262
283
|
Logger_1.default.info('APIRouter', `Schemas reloaded (added/updated: ${Object.keys(newSchemas).length}, removed: ${removed.join(', ') || 'none'})`);
|
|
284
|
+
Events_1.events.emitEvent(Events_1.Events.SCHEMA_UPDATE, { schemas: newSchemas, removed });
|
|
263
285
|
}
|
|
264
286
|
catch (err) {
|
|
265
287
|
Logger_1.default.error('APIRouter', 'Failed to refresh after schemasChanged', err);
|
|
@@ -310,15 +332,38 @@ class APIRouter {
|
|
|
310
332
|
return;
|
|
311
333
|
this.schemasRouteRegistered = true;
|
|
312
334
|
this.router.get('/_schemas', this.optionalAuthMiddleware, async (req, res) => {
|
|
313
|
-
|
|
314
|
-
//
|
|
315
|
-
const showPrivate = role === 'admin' || role === 'superadmin';
|
|
335
|
+
// Typically admin/superadmin can see clinical/private fields in schema.
|
|
336
|
+
// We now always include them in metadata so the frontend can decide visibility.
|
|
316
337
|
res.json({
|
|
317
338
|
success: true,
|
|
318
|
-
data: this.schemaLoader.getPublicSchemaList(
|
|
339
|
+
data: this.schemaLoader.getPublicSchemaList(true),
|
|
319
340
|
});
|
|
320
341
|
});
|
|
321
342
|
}
|
|
343
|
+
mountCleanupEndpointOnce() {
|
|
344
|
+
this.router.post('/_cleanup', this.optionalAuthMiddleware, async (req, res) => {
|
|
345
|
+
try {
|
|
346
|
+
if (typeof this.dbAdapter.cleanupUnusedFields !== 'function') {
|
|
347
|
+
return res.status(501).json({
|
|
348
|
+
success: false,
|
|
349
|
+
message: 'Database adapter does not support cleanup',
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
const report = await this.dbAdapter.cleanupUnusedFields(this.schemaLoader.getSchemas());
|
|
353
|
+
res.json({
|
|
354
|
+
success: true,
|
|
355
|
+
data: report,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
Logger_1.default.error('APIRouter', 'Cleanup failed', err);
|
|
360
|
+
res.status(500).json({
|
|
361
|
+
success: false,
|
|
362
|
+
message: err.message || 'Cleanup failed',
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
322
367
|
mountRoutes(schemas) {
|
|
323
368
|
if (!this.authRoutesInitialized && this.liveSchemas['users']) {
|
|
324
369
|
(0, setupAuthRoutes_1.setupAuthRoutes)(this.createCtx());
|
|
@@ -347,6 +392,13 @@ class APIRouter {
|
|
|
347
392
|
(0, mountSearchRoutes_1.mountSearchRoutes)(this.createCtx());
|
|
348
393
|
this.ensureNotFoundLast();
|
|
349
394
|
}
|
|
395
|
+
mountBatchRoutes() {
|
|
396
|
+
if (this.batchRoutesMounted)
|
|
397
|
+
return;
|
|
398
|
+
this.batchRoutesMounted = true;
|
|
399
|
+
(0, mountBatchRoutes_1.mountBatchRoutes)(this.createCtx());
|
|
400
|
+
this.ensureNotFoundLast();
|
|
401
|
+
}
|
|
350
402
|
mountFileRoutes() {
|
|
351
403
|
if (this.fileRoutesMounted)
|
|
352
404
|
return;
|
|
@@ -412,10 +464,13 @@ class APIRouter {
|
|
|
412
464
|
return;
|
|
413
465
|
}
|
|
414
466
|
if (error?.code === 11000) {
|
|
415
|
-
const field = Object.keys(error.keyPattern || {})
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
467
|
+
const field = Object.keys(error.keyPattern || error.keyValue || {}).find((k) => k !== '_id') ||
|
|
468
|
+
Object.keys(error.keyPattern || error.keyValue || {})[0] ||
|
|
469
|
+
'unknown';
|
|
470
|
+
res.status(400).json({
|
|
471
|
+
error: true,
|
|
472
|
+
message: `Duplicate value for field: ${field}. ${error.message || ''}`,
|
|
473
|
+
});
|
|
419
474
|
}
|
|
420
475
|
else {
|
|
421
476
|
res.status(400).json({ error: true, message: error?.message || 'Error' });
|
|
@@ -447,14 +502,20 @@ class APIRouter {
|
|
|
447
502
|
.map(([key]) => key);
|
|
448
503
|
const conflictingFields = [];
|
|
449
504
|
for (const field of uniqueFields) {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if (
|
|
456
|
-
|
|
505
|
+
const val = data[field];
|
|
506
|
+
const attr = schema.attributes?.[field];
|
|
507
|
+
const isRequired = Array.isArray(attr) ? attr[0]?.required : attr?.required;
|
|
508
|
+
// Sparse uniqueness: skip checking if value is empty/null AND the field is not required
|
|
509
|
+
if (val === undefined || val === null || (typeof val === 'string' && val.trim() === '')) {
|
|
510
|
+
if (!isRequired)
|
|
511
|
+
continue;
|
|
457
512
|
}
|
|
513
|
+
const query = { [field]: val };
|
|
514
|
+
if (excludeId)
|
|
515
|
+
query.id = { $ne: excludeId };
|
|
516
|
+
const existingRecords = await this.getModel(schema.modelName.toLowerCase()).read(query);
|
|
517
|
+
if (existingRecords.length > 0)
|
|
518
|
+
conflictingFields.push(field);
|
|
458
519
|
}
|
|
459
520
|
return conflictingFields.length > 0 ? conflictingFields : null;
|
|
460
521
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mountBatchRoutes = mountBatchRoutes;
|
|
4
|
+
const authorize_1 = require("../../policy/authorize");
|
|
5
|
+
const QueryEngine_1 = require("../../database/QueryEngine");
|
|
6
|
+
const utils_1 = require("./utils");
|
|
7
|
+
function mountBatchRoutes(ctx) {
|
|
8
|
+
const { router } = ctx;
|
|
9
|
+
router.post('/_batch', ctx.optionalAuthMiddleware, async (req, res) => {
|
|
10
|
+
try {
|
|
11
|
+
const batchRequest = req.body;
|
|
12
|
+
if (!batchRequest || typeof batchRequest !== 'object') {
|
|
13
|
+
return res.status(400).json({ error: true, message: 'Invalid batch request' });
|
|
14
|
+
}
|
|
15
|
+
const results = {};
|
|
16
|
+
for (const [key, config] of Object.entries(batchRequest)) {
|
|
17
|
+
try {
|
|
18
|
+
const { path, query: queryParams } = config;
|
|
19
|
+
if (!path) {
|
|
20
|
+
results[key] = { error: true, message: 'Missing path' };
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
// Path is usually something like "/doctors" or "/doctors?slug=... "
|
|
24
|
+
// We need to extract the model name
|
|
25
|
+
const modelMatch = path.match(/^\/([^/?]+)/);
|
|
26
|
+
if (!modelMatch) {
|
|
27
|
+
results[key] = { error: true, message: 'Invalid path' };
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const modelName = modelMatch[1].toLowerCase();
|
|
31
|
+
const schema = ctx.getSchema(modelName);
|
|
32
|
+
const model = ctx.getModel(modelName);
|
|
33
|
+
// Prepare pseudo-request for parseQuery
|
|
34
|
+
const pseudoReq = {
|
|
35
|
+
query: queryParams || {},
|
|
36
|
+
user: req.user,
|
|
37
|
+
};
|
|
38
|
+
const { limit, page, skip, sort, projection, sample, _populate } = (0, utils_1.parseQuery)(pseudoReq);
|
|
39
|
+
const filter = QueryEngine_1.QueryEngine.parse(pseudoReq.query, schema);
|
|
40
|
+
const roleStr = req.user?.role;
|
|
41
|
+
const pctx = {
|
|
42
|
+
isAuthenticated: !!req.user,
|
|
43
|
+
role: typeof roleStr === 'string' ? roleStr.toLowerCase() : null,
|
|
44
|
+
userId: req.user?.id || req.user?._id || null,
|
|
45
|
+
isSuperadmin: roleStr?.toLowerCase() === 'superadmin',
|
|
46
|
+
apiKeyOk: true,
|
|
47
|
+
};
|
|
48
|
+
const rdec = (0, authorize_1.authorize)(modelName, 'read', { allowedMethods: schema.allowedMethods, access: schema.access }, pctx);
|
|
49
|
+
if (!rdec.allow) {
|
|
50
|
+
results[key] = { error: true, message: 'Forbidden' };
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const finalFilter = { ...filter, ...(rdec.queryFilter || {}) };
|
|
54
|
+
// Handle countBy if present in query
|
|
55
|
+
const countBy = String(pseudoReq.query.countBy || '').trim();
|
|
56
|
+
if (countBy && schema.attributes[countBy]) {
|
|
57
|
+
const counts = await ctx.dbAdapter.countBy(schema.modelName, countBy, finalFilter, schema);
|
|
58
|
+
results[key] = { success: true, counts };
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const totalRows = await model.count(finalFilter);
|
|
62
|
+
const rows = await model.read(finalFilter, limit, skip, !!rdec.exposePrivate, {
|
|
63
|
+
sort,
|
|
64
|
+
projection,
|
|
65
|
+
sample,
|
|
66
|
+
_populate,
|
|
67
|
+
});
|
|
68
|
+
const data = rdec.exposePrivate
|
|
69
|
+
? (0, authorize_1.applyReadMaskMany)(rows, rdec.sensitiveMask)
|
|
70
|
+
: (0, authorize_1.applyReadMaskMany)(rows, rdec.readMask);
|
|
71
|
+
results[key] = {
|
|
72
|
+
success: true,
|
|
73
|
+
data,
|
|
74
|
+
pagination: { totalRows, page, limit },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
results[key] = { error: true, message: err.message || 'Error' };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return res.status(200).json({
|
|
82
|
+
success: true,
|
|
83
|
+
results,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
return res.status(500).json({ error: true, message: err.message || 'Batch execution failed' });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|