@airoom/nextmin-node 1.4.5 → 2.0.1

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.
Files changed (41) hide show
  1. package/README.md +48 -5
  2. package/dist/api/apiRouter.d.ts +2 -0
  3. package/dist/api/apiRouter.js +68 -19
  4. package/dist/api/router/mountCrudRoutes.js +209 -221
  5. package/dist/api/router/mountFindRoutes.js +2 -49
  6. package/dist/api/router/mountSearchRoutes.js +10 -52
  7. package/dist/api/router/mountSearchRoutes_extended.js +7 -48
  8. package/dist/api/router/setupAuthRoutes.js +6 -2
  9. package/dist/api/router/utils.js +20 -7
  10. package/dist/cli.d.ts +1 -0
  11. package/dist/cli.js +83 -0
  12. package/dist/database/DatabaseAdapter.d.ts +7 -0
  13. package/dist/database/NMAdapter.d.ts +41 -0
  14. package/dist/database/NMAdapter.js +979 -0
  15. package/dist/database/QueryEngine.d.ts +14 -0
  16. package/dist/database/QueryEngine.js +215 -0
  17. package/dist/database/utils.d.ts +2 -0
  18. package/dist/database/utils.js +21 -0
  19. package/dist/index.d.ts +4 -1
  20. package/dist/index.js +11 -5
  21. package/dist/models/BaseModel.d.ts +16 -0
  22. package/dist/models/BaseModel.js +32 -4
  23. package/dist/policy/authorize.js +118 -43
  24. package/dist/schemas/Users.json +66 -30
  25. package/dist/services/RealtimeService.d.ts +20 -0
  26. package/dist/services/RealtimeService.js +93 -0
  27. package/dist/services/SchemaService.d.ts +3 -0
  28. package/dist/services/SchemaService.js +9 -5
  29. package/dist/utils/DefaultDataInitializer.js +10 -2
  30. package/dist/utils/Events.d.ts +34 -0
  31. package/dist/utils/Events.js +55 -0
  32. package/dist/utils/Logger.js +12 -10
  33. package/dist/utils/QueryCache.d.ts +16 -0
  34. package/dist/utils/QueryCache.js +106 -0
  35. package/dist/utils/SchemaLoader.d.ts +7 -2
  36. package/dist/utils/SchemaLoader.js +58 -18
  37. package/package.json +19 -4
  38. package/dist/database/InMemoryAdapter.d.ts +0 -15
  39. package/dist/database/InMemoryAdapter.js +0 -71
  40. package/dist/database/MongoAdapter.d.ts +0 -52
  41. package/dist/database/MongoAdapter.js +0 -410
@@ -38,6 +38,23 @@ function computeSensitiveMask(schemaPolicy) {
38
38
  return negs;
39
39
  }
40
40
  /* ----------------- general helpers ----------------- */
41
+ function getPolicySection(base, ...path) {
42
+ if (!base)
43
+ return undefined;
44
+ let curr = base;
45
+ for (let i = 0; i < path.length; i++) {
46
+ const part = path[i];
47
+ // try exact key (e.g. "roles.admin")
48
+ const flatKey = path.slice(i).join('.');
49
+ if (curr[flatKey] !== undefined)
50
+ return curr[flatKey];
51
+ // try nested
52
+ curr = curr[part];
53
+ if (curr === undefined)
54
+ return undefined;
55
+ }
56
+ return curr;
57
+ }
41
58
  function asBool(v) {
42
59
  return typeof v === 'boolean' ? v : undefined;
43
60
  }
@@ -61,6 +78,41 @@ function isOwnerDoc(schemaPolicy, ctx, doc) {
61
78
  const candidate = typeof val === 'object' ? (val.id ?? val._id ?? String(val)) : String(val);
62
79
  return String(candidate) === String(ctx.userId);
63
80
  }
81
+ const OPERATOR_MAP = {
82
+ ne: '$ne',
83
+ gt: '$gt',
84
+ gte: '$gte',
85
+ lt: '$lt',
86
+ lte: '$lte',
87
+ in: '$in',
88
+ nin: '$nin',
89
+ regex: '$regex',
90
+ options: '$options',
91
+ and: '$and',
92
+ or: '$or',
93
+ nor: '$nor',
94
+ };
95
+ function interpolate(val, ctx) {
96
+ if (typeof val === 'string') {
97
+ if (val === '$CTX.userId')
98
+ return ctx.userId;
99
+ if (val === '$CTX.role')
100
+ return ctx.role;
101
+ return val;
102
+ }
103
+ if (Array.isArray(val)) {
104
+ return val.map((v) => interpolate(v, ctx));
105
+ }
106
+ if (val && typeof val === 'object' && val.constructor === Object) {
107
+ const out = {};
108
+ for (const [k, v] of Object.entries(val)) {
109
+ const targetKey = OPERATOR_MAP[k] || k;
110
+ out[targetKey] = interpolate(v, ctx);
111
+ }
112
+ return out;
113
+ }
114
+ return val;
115
+ }
64
116
  /* =================================================== */
65
117
  /* =============== MAIN AUTHORIZATION ================= */
66
118
  /* =================================================== */
@@ -71,6 +123,15 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
71
123
  return { ...EMPTY_DECISION, allow: false };
72
124
  const access = schemaPolicy?.access || {};
73
125
  const baseReadMask = [].concat(access?.readMask || []);
126
+ // Auto-mask private attributes
127
+ for (const [name, attr] of Object.entries(schemaPolicy?.attributes || {})) {
128
+ const a = Array.isArray(attr) ? attr[0] : attr;
129
+ if (a && a.private === true) {
130
+ const mask = `-${name}`;
131
+ if (!baseReadMask.includes(mask))
132
+ baseReadMask.push(mask);
133
+ }
134
+ }
74
135
  const baseWriteDeny = [].concat(access?.writeDeny || []);
75
136
  const restrictionsBase = access?.restrictions || {};
76
137
  const createDefaultsBase = access?.createDefaults || {};
@@ -83,27 +144,47 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
83
144
  const effectiveWriteDeny = bypass ? [] : writeDenyIn || [];
84
145
  return { effectiveReadMask, effectiveWriteDeny };
85
146
  };
86
- // 1) no access block sensible defaults
87
- if (!schemaPolicy?.access) {
88
- const allowedByDefault = action === 'read' ? true : !!ctx.isAuthenticated;
89
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
147
+ const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
148
+ const finalizeFallback = (dec) => {
90
149
  return {
150
+ ...dec,
151
+ queryFilter: interpolate(dec.queryFilter, ctx),
152
+ createDefaults: interpolate(dec.createDefaults, ctx),
153
+ restrictions: interpolate(dec.restrictions, ctx),
154
+ };
155
+ };
156
+ // 1) Reachability check: Do we have ANY explicit rules for this action?
157
+ const hasReachabilityRule = !!(access?.public?.[action] !== undefined ||
158
+ access?.authenticated?.[action] !== undefined ||
159
+ (access?.roles &&
160
+ Object.values(access.roles).some((r) => r[action] !== undefined)));
161
+ // If no access block OR no reachability rule for this specific action → sensible defaults
162
+ if (!schemaPolicy?.access || !hasReachabilityRule) {
163
+ const allowedByDefault = action === 'read' ? true : !!ctx.isAuthenticated;
164
+ return finalizeFallback({
91
165
  allow: allowedByDefault,
92
166
  readMask: effectiveReadMask,
93
167
  writeDeny: effectiveWriteDeny,
94
168
  createDefaults: {},
95
169
  restrictions: {},
96
170
  queryFilter: action === 'read' ? {} : {},
97
- exposePrivate: bypass, // will normally be false if no bypassPrivacy set
171
+ exposePrivate: bypass,
98
172
  sensitiveMask,
99
- };
173
+ });
100
174
  }
175
+ const finalize = (dec) => {
176
+ return {
177
+ ...dec,
178
+ queryFilter: interpolate(dec.queryFilter, ctx),
179
+ createDefaults: interpolate(dec.createDefaults, ctx),
180
+ restrictions: interpolate(dec.restrictions, ctx),
181
+ };
182
+ };
101
183
  // 2) PUBLIC
102
184
  const pubRule = access?.public?.[action];
103
185
  const pubBool = asBool(pubRule);
104
186
  if (pubBool === true) {
105
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
106
- return {
187
+ return finalize({
107
188
  allow: true,
108
189
  readMask: effectiveReadMask,
109
190
  writeDeny: effectiveWriteDeny,
@@ -112,43 +193,41 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
112
193
  queryFilter: queryFilterBase?.public || {},
113
194
  exposePrivate: bypass, // if caller has a bypass role, they get private fields (still masked by sensitive)
114
195
  sensitiveMask,
115
- };
196
+ });
116
197
  }
117
198
  // if false → continue to roles/auth checks
118
199
  // 3) ROLES
119
200
  const roleName = (ctx.role || '').toLowerCase();
120
- if (roleName && access?.roles?.[roleName]) {
121
- const rule = access.roles[roleName][action];
201
+ const roleRules = roleName ? getPolicySection(access, 'roles', roleName) : null;
202
+ if (roleRules) {
203
+ const rule = roleRules[action];
122
204
  const rBool = asBool(rule);
123
205
  if (rBool === true) {
124
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
125
- return {
206
+ return finalize({
126
207
  allow: true,
127
208
  readMask: effectiveReadMask,
128
209
  writeDeny: effectiveWriteDeny,
129
- createDefaults: createDefaultsBase?.roles?.[roleName] || {},
130
- restrictions: restrictionsBase?.roles?.[roleName] || {},
131
- queryFilter: queryFilterBase?.roles?.[roleName] || {},
210
+ createDefaults: getPolicySection(createDefaultsBase, 'roles', roleName) || {},
211
+ restrictions: getPolicySection(restrictionsBase, 'roles', roleName) || {},
212
+ queryFilter: getPolicySection(queryFilterBase, 'roles', roleName) || {},
132
213
  exposePrivate: bypass,
133
214
  sensitiveMask,
134
- };
215
+ });
135
216
  }
136
217
  if (rBool === false) {
137
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
138
- return {
218
+ return finalize({
139
219
  ...EMPTY_DECISION,
140
220
  readMask: effectiveReadMask,
141
221
  writeDeny: effectiveWriteDeny,
142
222
  exposePrivate: false,
143
223
  sensitiveMask,
144
- };
224
+ });
145
225
  }
146
226
  // role === 'owner'
147
227
  if (rule === 'owner') {
148
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
149
228
  if (action === 'read') {
150
229
  if (doc) {
151
- return {
230
+ return finalize({
152
231
  allow: isOwnerDoc(schemaPolicy, ctx, doc),
153
232
  readMask: effectiveReadMask,
154
233
  writeDeny: effectiveWriteDeny,
@@ -157,9 +236,9 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
157
236
  queryFilter: {},
158
237
  exposePrivate: bypass,
159
238
  sensitiveMask,
160
- };
239
+ });
161
240
  }
162
- return {
241
+ return finalize({
163
242
  allow: true,
164
243
  readMask: effectiveReadMask,
165
244
  writeDeny: effectiveWriteDeny,
@@ -168,9 +247,9 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
168
247
  queryFilter: ownerFilter(schemaPolicy, ctx),
169
248
  exposePrivate: bypass,
170
249
  sensitiveMask,
171
- };
250
+ });
172
251
  }
173
- return {
252
+ return finalize({
174
253
  allow: isOwnerDoc(schemaPolicy, ctx, doc),
175
254
  readMask: effectiveReadMask,
176
255
  writeDeny: effectiveWriteDeny,
@@ -179,15 +258,14 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
179
258
  queryFilter: {},
180
259
  exposePrivate: bypass,
181
260
  sensitiveMask,
182
- };
261
+ });
183
262
  }
184
263
  }
185
264
  // 4) AUTHENTICATED
186
265
  const authRule = access?.authenticated?.[action];
187
266
  const aBool = asBool(authRule);
188
267
  if (aBool === true && ctx.isAuthenticated) {
189
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
190
- return {
268
+ return finalize({
191
269
  allow: true,
192
270
  readMask: effectiveReadMask,
193
271
  writeDeny: effectiveWriteDeny,
@@ -196,23 +274,21 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
196
274
  queryFilter: queryFilterBase?.authenticated || {},
197
275
  exposePrivate: bypass,
198
276
  sensitiveMask,
199
- };
277
+ });
200
278
  }
201
279
  if (aBool === false && ctx.isAuthenticated) {
202
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
203
- return {
280
+ return finalize({
204
281
  ...EMPTY_DECISION,
205
282
  readMask: effectiveReadMask,
206
283
  writeDeny: effectiveWriteDeny,
207
284
  exposePrivate: false,
208
285
  sensitiveMask,
209
- };
286
+ });
210
287
  }
211
288
  if (authRule === 'owner' && ctx.isAuthenticated) {
212
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
213
289
  if (action === 'read') {
214
290
  if (doc) {
215
- return {
291
+ return finalize({
216
292
  allow: isOwnerDoc(schemaPolicy, ctx, doc),
217
293
  readMask: effectiveReadMask,
218
294
  writeDeny: effectiveWriteDeny,
@@ -221,9 +297,9 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
221
297
  queryFilter: {},
222
298
  exposePrivate: bypass,
223
299
  sensitiveMask,
224
- };
300
+ });
225
301
  }
226
- return {
302
+ return finalize({
227
303
  allow: true,
228
304
  readMask: effectiveReadMask,
229
305
  writeDeny: effectiveWriteDeny,
@@ -232,9 +308,9 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
232
308
  queryFilter: ownerFilter(schemaPolicy, ctx),
233
309
  exposePrivate: bypass,
234
310
  sensitiveMask,
235
- };
311
+ });
236
312
  }
237
- return {
313
+ return finalize({
238
314
  allow: isOwnerDoc(schemaPolicy, ctx, doc),
239
315
  readMask: effectiveReadMask,
240
316
  writeDeny: effectiveReadMask, // (typo guard: will be ignored by callers; leave as is)
@@ -243,17 +319,16 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
243
319
  queryFilter: {},
244
320
  exposePrivate: bypass,
245
321
  sensitiveMask,
246
- };
322
+ });
247
323
  }
248
324
  // 5) default deny
249
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
250
- return {
325
+ return finalize({
251
326
  ...EMPTY_DECISION,
252
327
  readMask: effectiveReadMask,
253
328
  writeDeny: effectiveWriteDeny,
254
329
  exposePrivate: false,
255
330
  sensitiveMask,
256
- };
331
+ });
257
332
  }
258
333
  /* ===== Helpers consumed by the router ===== */
259
334
  function applyReadMaskOne(doc, mask) {
@@ -4,12 +4,15 @@
4
4
  "username": {
5
5
  "type": "string",
6
6
  "required": true,
7
- "unique": true
7
+ "unique": true,
8
+ "safe": true
8
9
  },
9
10
  "email": {
10
11
  "type": "string",
11
12
  "required": true,
12
- "unique": true
13
+ "unique": true,
14
+ "private": true,
15
+ "safe": true
13
16
  },
14
17
  "firstName": {
15
18
  "type": "string",
@@ -22,9 +25,9 @@
22
25
  "password": {
23
26
  "type": "string",
24
27
  "required": true,
25
- "private": true
28
+ "private": true,
29
+ "safe": true
26
30
  },
27
-
28
31
  "profilePicture": [
29
32
  {
30
33
  "type": "string",
@@ -35,7 +38,6 @@
35
38
  "required": true
36
39
  }
37
40
  ],
38
-
39
41
  "role": [
40
42
  {
41
43
  "type": "ObjectId",
@@ -45,28 +47,35 @@
45
47
  "required": true
46
48
  }
47
49
  ],
48
-
49
50
  "status": {
50
51
  "type": "string",
51
- "enum": ["pending", "active", "suspended"],
52
+ "enum": [
53
+ "pending",
54
+ "active",
55
+ "suspended"
56
+ ],
52
57
  "default": "pending",
53
- "required": true
58
+ "required": true,
59
+ "safe": true
54
60
  },
55
61
  "type": {
56
62
  "type": "string",
57
- "enum": ["system", "default", "user"],
63
+ "enum": [
64
+ "system",
65
+ "default",
66
+ "user"
67
+ ],
58
68
  "default": "user",
59
- "required": true
69
+ "required": true,
70
+ "safe": true
60
71
  }
61
72
  },
62
-
63
73
  "allowedMethods": {
64
74
  "create": true,
65
75
  "read": true,
66
76
  "update": true,
67
77
  "delete": false
68
78
  },
69
-
70
79
  "access": {
71
80
  "public": {
72
81
  "create": true,
@@ -77,10 +86,9 @@
77
86
  "authenticated": {
78
87
  "create": false,
79
88
  "read": "owner",
80
- "update": false,
89
+ "update": "owner",
81
90
  "delete": false
82
91
  },
83
-
84
92
  "roles": {
85
93
  "admin": {
86
94
  "create": true,
@@ -95,28 +103,56 @@
95
103
  "delete": true
96
104
  }
97
105
  },
98
-
99
106
  "queryFilter": {
100
- "authenticated": { "id": { "ne": "$CTX.userId" } },
107
+ "authenticated": {
108
+ "id": {
109
+ "ne": "$CTX.userId"
110
+ }
111
+ },
101
112
  "roles": {
102
- "admin": { "id": { "ne": "$CTX.userId" } },
103
- "superadmin": {}
113
+ "admin": {
114
+ "id": {
115
+ "ne": "$CTX.userId"
116
+ }
117
+ },
118
+ "superadmin": {}
119
+ }
120
+ },
121
+ "conditions": {
122
+ "owner": {
123
+ "by": "id"
104
124
  }
105
125
  },
106
-
107
- "conditions": { "owner": { "by": "id" } },
108
- "readMask": ["-password"],
109
- "writeDeny": ["password"],
110
- "bypassPrivacy": { "roles": ["admin", "superadmin"] },
111
-
126
+ "readMask": [
127
+ "-password"
128
+ ],
129
+ "writeDeny": [
130
+ "password"
131
+ ],
132
+ "bypassPrivacy": {
133
+ "roles": [
134
+ "admin",
135
+ "superadmin"
136
+ ]
137
+ },
112
138
  "createDefaults": {
113
- "public": { "role": "user", "status": "pending" },
114
- "roles.admin": { "status": "active" },
115
- "roles.superadmin": { "status": "active" }
139
+ "public": {
140
+ "role": "user",
141
+ "status": "pending"
142
+ },
143
+ "roles.admin": {
144
+ "status": "active"
145
+ },
146
+ "roles.superadmin": {
147
+ "status": "active"
148
+ }
116
149
  },
117
-
118
150
  "restrictions": {
119
- "roles.admin.cannotAssign": { "role": ["superadmin"] }
151
+ "roles.admin.cannotAssign": {
152
+ "role": [
153
+ "superadmin"
154
+ ]
155
+ }
120
156
  }
121
157
  }
122
- }
158
+ }
@@ -0,0 +1,20 @@
1
+ import { Server as HTTPServer } from 'http';
2
+ export declare const SOCKET_PATH = "/__nextmin__/realtime";
3
+ export declare const NAMESPACE = "/realtime";
4
+ /**
5
+ * RealtimeService handles real-time communication with clients using Socket.io.
6
+ * It broadcasts schema changes and CRUD events.
7
+ */
8
+ export declare class RealtimeService {
9
+ private opts;
10
+ private io;
11
+ private nsp;
12
+ constructor(server: HTTPServer, opts: {
13
+ getApiKey?: () => string | undefined;
14
+ corsOrigin?: string | any;
15
+ });
16
+ private setupAuth;
17
+ private setupListeners;
18
+ private forwardSystemEvents;
19
+ shutdown(): void;
20
+ }
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RealtimeService = exports.NAMESPACE = exports.SOCKET_PATH = void 0;
7
+ const socket_io_1 = require("socket.io");
8
+ const SchemaLoader_1 = require("../utils/SchemaLoader");
9
+ const Events_1 = require("../utils/Events");
10
+ const Logger_1 = __importDefault(require("../utils/Logger"));
11
+ exports.SOCKET_PATH = '/__nextmin__/realtime';
12
+ exports.NAMESPACE = '/realtime';
13
+ /**
14
+ * RealtimeService handles real-time communication with clients using Socket.io.
15
+ * It broadcasts schema changes and CRUD events.
16
+ */
17
+ class RealtimeService {
18
+ constructor(server, opts) {
19
+ this.opts = opts;
20
+ this.io = new socket_io_1.Server(server, {
21
+ path: exports.SOCKET_PATH,
22
+ cors: { origin: opts.corsOrigin ?? '*' },
23
+ });
24
+ this.nsp = this.io.of(exports.NAMESPACE);
25
+ this.setupAuth();
26
+ this.setupListeners();
27
+ this.forwardSystemEvents();
28
+ Logger_1.default.info('RealtimeService', `Initialized at ${exports.SOCKET_PATH}`);
29
+ }
30
+ setupAuth() {
31
+ this.nsp.use((socket, next) => {
32
+ const provided = socket.handshake.auth?.apiKey;
33
+ const trusted = this.opts.getApiKey?.();
34
+ if (trusted && provided === trusted)
35
+ return next();
36
+ next(new Error('unauthorized'));
37
+ });
38
+ }
39
+ setupListeners() {
40
+ this.nsp.on('connection', (socket) => {
41
+ Logger_1.default.info('RealtimeService', `Client connected: ${socket.id}`);
42
+ const loader = SchemaLoader_1.SchemaLoader.getInstance();
43
+ socket.emit('schemasData', loader.getPublicSchemaList(true));
44
+ });
45
+ }
46
+ forwardSystemEvents() {
47
+ // 1. Forward Schema changes
48
+ const loader = SchemaLoader_1.SchemaLoader.getInstance();
49
+ if (typeof loader.on === 'function') {
50
+ loader.on('schemasChanged', () => {
51
+ this.nsp.emit('schemasUpdated', loader.getPublicSchemaList(true));
52
+ });
53
+ }
54
+ // 2. Forward CRUD events (Events from our central emitter)
55
+ Events_1.events.on(Events_1.Events.AFTER_CREATE, (payload) => {
56
+ const model = payload.modelName.toLowerCase();
57
+ const data = {
58
+ model: payload.modelName,
59
+ action: 'created',
60
+ id: payload.result.id,
61
+ data: payload.result
62
+ };
63
+ this.nsp.emit('doc:created', data);
64
+ this.nsp.emit(`${model}:created`, data);
65
+ });
66
+ Events_1.events.on(Events_1.Events.AFTER_UPDATE, (payload) => {
67
+ const model = payload.modelName.toLowerCase();
68
+ const data = {
69
+ model: payload.modelName,
70
+ action: 'updated',
71
+ id: payload.id,
72
+ data: payload.result
73
+ };
74
+ this.nsp.emit('doc:updated', data);
75
+ this.nsp.emit(`${model}:updated`, data);
76
+ });
77
+ Events_1.events.on(Events_1.Events.AFTER_DELETE, (payload) => {
78
+ const model = payload.modelName.toLowerCase();
79
+ const data = {
80
+ model: payload.modelName,
81
+ action: 'deleted',
82
+ id: payload.id
83
+ };
84
+ this.nsp.emit('doc:deleted', data);
85
+ this.nsp.emit(`${model}:deleted`, data);
86
+ });
87
+ }
88
+ shutdown() {
89
+ this.io.close();
90
+ Logger_1.default.warn('RealtimeService', 'Stopped');
91
+ }
92
+ }
93
+ exports.RealtimeService = RealtimeService;
@@ -7,4 +7,7 @@ export interface SchemaServiceOptions {
7
7
  /** Optional CORS origin override (defaults to "*") */
8
8
  corsOrigin?: string | RegExp | (string | RegExp)[];
9
9
  }
10
+ /**
11
+ * @deprecated The SchemaService is deprecated. Use RealtimeService instead.
12
+ */
10
13
  export declare function startSchemaService(server: HTTPServer, opts?: SchemaServiceOptions): void;
@@ -13,7 +13,11 @@ exports.NAMESPACE = '/schema';
13
13
  let started = false;
14
14
  let io = null;
15
15
  let nsp = null;
16
+ /**
17
+ * @deprecated The SchemaService is deprecated. Use RealtimeService instead.
18
+ */
16
19
  function startSchemaService(server, opts = {}) {
20
+ Logger_1.default.warn('SchemaService', 'The SchemaService is deprecated. Use RealtimeService instead.');
17
21
  if (started)
18
22
  return;
19
23
  started = true;
@@ -34,13 +38,13 @@ function startSchemaService(server, opts = {}) {
34
38
  const loader = SchemaLoader_1.SchemaLoader.getInstance?.() ?? new SchemaLoader_1.SchemaLoader();
35
39
  nsp.on('connection', (socket) => {
36
40
  Logger_1.default.info('SchemaService:', socket.id);
37
- // Send full snapshot on connect
38
- socket.emit('schemasData', loader.getPublicSchemaList());
41
+ // Send normalized (secure) snapshot on connect
42
+ socket.emit('schemasData', loader.getPublicSchemaList(true));
39
43
  });
40
- // Broadcast updates on hot-reload / schema changes
44
+ // Broadcast sanitized updates on hot-reload / schema changes
41
45
  if (typeof loader.on === 'function') {
42
- loader.on('schemasChanged', (schemas) => {
43
- nsp?.emit('schemasUpdated', schemas);
46
+ loader.on('schemasChanged', () => {
47
+ nsp?.emit('schemasUpdated', loader.getPublicSchemaList(true));
44
48
  });
45
49
  }
46
50
  }
@@ -235,8 +235,16 @@ class DefaultDataInitializer {
235
235
  // Update only missing/empty fields (do not overwrite existing values)
236
236
  const updates = {};
237
237
  // apiKey
238
- if (doc.apiKey && String(doc.apiKey).trim().length > 0) {
239
- this.apiKey = doc.apiKey; // honor existing
238
+ const envKey = process.env.NEXTMIN_API_KEY;
239
+ if (envKey && String(envKey).trim().length > 0) {
240
+ // If environment variable is set, it takes precedence over the database
241
+ this.apiKey = envKey;
242
+ if (doc.apiKey !== envKey) {
243
+ updates.apiKey = envKey;
244
+ }
245
+ }
246
+ else if (doc.apiKey && String(doc.apiKey).trim().length > 0) {
247
+ this.apiKey = doc.apiKey; // honor existing if no env var
240
248
  }
241
249
  else {
242
250
  updates.apiKey = generatedApiKey;
@@ -0,0 +1,34 @@
1
+ import { EventEmitter } from 'events';
2
+ /**
3
+ * NextMinEvents is a central event emitter for the NextMin ecosystem.
4
+ * It allows components to hook into CRUD and system events.
5
+ */
6
+ declare class NextMinEvents extends EventEmitter {
7
+ constructor();
8
+ /**
9
+ * Typed emit to provide better DX (Developer Experience)
10
+ */
11
+ emitEvent(event: string, payload: any): void;
12
+ }
13
+ export declare const events: NextMinEvents;
14
+ export declare const Events: {
15
+ BEFORE_CREATE: string;
16
+ AFTER_CREATE: string;
17
+ BEFORE_UPDATE: string;
18
+ AFTER_UPDATE: string;
19
+ BEFORE_DELETE: string;
20
+ AFTER_DELETE: string;
21
+ BEFORE_READ: string;
22
+ AFTER_READ: string;
23
+ AUTH_LOGIN: string;
24
+ AUTH_LOGOUT: string;
25
+ AUTH_SIGNUP: string;
26
+ SCHEMA_UPDATE: string;
27
+ SERVER_START: string;
28
+ SERVER_STOP: string;
29
+ };
30
+ /**
31
+ * Helper to generate model-specific event names
32
+ */
33
+ export declare function getModelEvent(modelName: string, action: 'create' | 'read' | 'update' | 'delete', phase?: 'before' | 'after'): string;
34
+ export {};