@airoom/nextmin-node 1.4.6 → 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 (40) hide show
  1. package/README.md +48 -5
  2. package/dist/api/apiRouter.d.ts +2 -0
  3. package/dist/api/apiRouter.js +67 -21
  4. package/dist/api/router/mountCrudRoutes.js +207 -220
  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/utils.js +20 -7
  9. package/dist/cli.d.ts +1 -0
  10. package/dist/cli.js +83 -0
  11. package/dist/database/DatabaseAdapter.d.ts +7 -0
  12. package/dist/database/NMAdapter.d.ts +41 -0
  13. package/dist/database/NMAdapter.js +979 -0
  14. package/dist/database/QueryEngine.d.ts +14 -0
  15. package/dist/database/QueryEngine.js +215 -0
  16. package/dist/database/utils.d.ts +2 -0
  17. package/dist/database/utils.js +21 -0
  18. package/dist/index.d.ts +4 -1
  19. package/dist/index.js +11 -5
  20. package/dist/models/BaseModel.d.ts +16 -0
  21. package/dist/models/BaseModel.js +32 -4
  22. package/dist/policy/authorize.js +95 -38
  23. package/dist/schemas/Users.json +66 -30
  24. package/dist/services/RealtimeService.d.ts +20 -0
  25. package/dist/services/RealtimeService.js +93 -0
  26. package/dist/services/SchemaService.d.ts +3 -0
  27. package/dist/services/SchemaService.js +6 -2
  28. package/dist/utils/DefaultDataInitializer.js +10 -2
  29. package/dist/utils/Events.d.ts +34 -0
  30. package/dist/utils/Events.js +55 -0
  31. package/dist/utils/Logger.js +12 -10
  32. package/dist/utils/QueryCache.d.ts +16 -0
  33. package/dist/utils/QueryCache.js +106 -0
  34. package/dist/utils/SchemaLoader.d.ts +5 -0
  35. package/dist/utils/SchemaLoader.js +45 -3
  36. package/package.json +19 -4
  37. package/dist/database/InMemoryAdapter.d.ts +0 -15
  38. package/dist/database/InMemoryAdapter.js +0 -71
  39. package/dist/database/MongoAdapter.d.ts +0 -52
  40. package/dist/database/MongoAdapter.js +0 -410
@@ -78,6 +78,41 @@ function isOwnerDoc(schemaPolicy, ctx, doc) {
78
78
  const candidate = typeof val === 'object' ? (val.id ?? val._id ?? String(val)) : String(val);
79
79
  return String(candidate) === String(ctx.userId);
80
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
+ }
81
116
  /* =================================================== */
82
117
  /* =============== MAIN AUTHORIZATION ================= */
83
118
  /* =================================================== */
@@ -88,6 +123,15 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
88
123
  return { ...EMPTY_DECISION, allow: false };
89
124
  const access = schemaPolicy?.access || {};
90
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
+ }
91
135
  const baseWriteDeny = [].concat(access?.writeDeny || []);
92
136
  const restrictionsBase = access?.restrictions || {};
93
137
  const createDefaultsBase = access?.createDefaults || {};
@@ -100,27 +144,47 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
100
144
  const effectiveWriteDeny = bypass ? [] : writeDenyIn || [];
101
145
  return { effectiveReadMask, effectiveWriteDeny };
102
146
  };
103
- // 1) no access block sensible defaults
104
- if (!schemaPolicy?.access) {
105
- const allowedByDefault = action === 'read' ? true : !!ctx.isAuthenticated;
106
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
147
+ const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
148
+ const finalizeFallback = (dec) => {
107
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({
108
165
  allow: allowedByDefault,
109
166
  readMask: effectiveReadMask,
110
167
  writeDeny: effectiveWriteDeny,
111
168
  createDefaults: {},
112
169
  restrictions: {},
113
170
  queryFilter: action === 'read' ? {} : {},
114
- exposePrivate: bypass, // will normally be false if no bypassPrivacy set
171
+ exposePrivate: bypass,
115
172
  sensitiveMask,
116
- };
173
+ });
117
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
+ };
118
183
  // 2) PUBLIC
119
184
  const pubRule = access?.public?.[action];
120
185
  const pubBool = asBool(pubRule);
121
186
  if (pubBool === true) {
122
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
123
- return {
187
+ return finalize({
124
188
  allow: true,
125
189
  readMask: effectiveReadMask,
126
190
  writeDeny: effectiveWriteDeny,
@@ -129,7 +193,7 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
129
193
  queryFilter: queryFilterBase?.public || {},
130
194
  exposePrivate: bypass, // if caller has a bypass role, they get private fields (still masked by sensitive)
131
195
  sensitiveMask,
132
- };
196
+ });
133
197
  }
134
198
  // if false → continue to roles/auth checks
135
199
  // 3) ROLES
@@ -139,8 +203,7 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
139
203
  const rule = roleRules[action];
140
204
  const rBool = asBool(rule);
141
205
  if (rBool === true) {
142
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
143
- return {
206
+ return finalize({
144
207
  allow: true,
145
208
  readMask: effectiveReadMask,
146
209
  writeDeny: effectiveWriteDeny,
@@ -149,24 +212,22 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
149
212
  queryFilter: getPolicySection(queryFilterBase, 'roles', roleName) || {},
150
213
  exposePrivate: bypass,
151
214
  sensitiveMask,
152
- };
215
+ });
153
216
  }
154
217
  if (rBool === false) {
155
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
156
- return {
218
+ return finalize({
157
219
  ...EMPTY_DECISION,
158
220
  readMask: effectiveReadMask,
159
221
  writeDeny: effectiveWriteDeny,
160
222
  exposePrivate: false,
161
223
  sensitiveMask,
162
- };
224
+ });
163
225
  }
164
226
  // role === 'owner'
165
227
  if (rule === 'owner') {
166
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
167
228
  if (action === 'read') {
168
229
  if (doc) {
169
- return {
230
+ return finalize({
170
231
  allow: isOwnerDoc(schemaPolicy, ctx, doc),
171
232
  readMask: effectiveReadMask,
172
233
  writeDeny: effectiveWriteDeny,
@@ -175,9 +236,9 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
175
236
  queryFilter: {},
176
237
  exposePrivate: bypass,
177
238
  sensitiveMask,
178
- };
239
+ });
179
240
  }
180
- return {
241
+ return finalize({
181
242
  allow: true,
182
243
  readMask: effectiveReadMask,
183
244
  writeDeny: effectiveWriteDeny,
@@ -186,9 +247,9 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
186
247
  queryFilter: ownerFilter(schemaPolicy, ctx),
187
248
  exposePrivate: bypass,
188
249
  sensitiveMask,
189
- };
250
+ });
190
251
  }
191
- return {
252
+ return finalize({
192
253
  allow: isOwnerDoc(schemaPolicy, ctx, doc),
193
254
  readMask: effectiveReadMask,
194
255
  writeDeny: effectiveWriteDeny,
@@ -197,15 +258,14 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
197
258
  queryFilter: {},
198
259
  exposePrivate: bypass,
199
260
  sensitiveMask,
200
- };
261
+ });
201
262
  }
202
263
  }
203
264
  // 4) AUTHENTICATED
204
265
  const authRule = access?.authenticated?.[action];
205
266
  const aBool = asBool(authRule);
206
267
  if (aBool === true && ctx.isAuthenticated) {
207
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
208
- return {
268
+ return finalize({
209
269
  allow: true,
210
270
  readMask: effectiveReadMask,
211
271
  writeDeny: effectiveWriteDeny,
@@ -214,23 +274,21 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
214
274
  queryFilter: queryFilterBase?.authenticated || {},
215
275
  exposePrivate: bypass,
216
276
  sensitiveMask,
217
- };
277
+ });
218
278
  }
219
279
  if (aBool === false && ctx.isAuthenticated) {
220
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
221
- return {
280
+ return finalize({
222
281
  ...EMPTY_DECISION,
223
282
  readMask: effectiveReadMask,
224
283
  writeDeny: effectiveWriteDeny,
225
284
  exposePrivate: false,
226
285
  sensitiveMask,
227
- };
286
+ });
228
287
  }
229
288
  if (authRule === 'owner' && ctx.isAuthenticated) {
230
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
231
289
  if (action === 'read') {
232
290
  if (doc) {
233
- return {
291
+ return finalize({
234
292
  allow: isOwnerDoc(schemaPolicy, ctx, doc),
235
293
  readMask: effectiveReadMask,
236
294
  writeDeny: effectiveWriteDeny,
@@ -239,9 +297,9 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
239
297
  queryFilter: {},
240
298
  exposePrivate: bypass,
241
299
  sensitiveMask,
242
- };
300
+ });
243
301
  }
244
- return {
302
+ return finalize({
245
303
  allow: true,
246
304
  readMask: effectiveReadMask,
247
305
  writeDeny: effectiveWriteDeny,
@@ -250,9 +308,9 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
250
308
  queryFilter: ownerFilter(schemaPolicy, ctx),
251
309
  exposePrivate: bypass,
252
310
  sensitiveMask,
253
- };
311
+ });
254
312
  }
255
- return {
313
+ return finalize({
256
314
  allow: isOwnerDoc(schemaPolicy, ctx, doc),
257
315
  readMask: effectiveReadMask,
258
316
  writeDeny: effectiveReadMask, // (typo guard: will be ignored by callers; leave as is)
@@ -261,17 +319,16 @@ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
261
319
  queryFilter: {},
262
320
  exposePrivate: bypass,
263
321
  sensitiveMask,
264
- };
322
+ });
265
323
  }
266
324
  // 5) default deny
267
- const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
268
- return {
325
+ return finalize({
269
326
  ...EMPTY_DECISION,
270
327
  readMask: effectiveReadMask,
271
328
  writeDeny: effectiveWriteDeny,
272
329
  exposePrivate: false,
273
330
  sensitiveMask,
274
- };
331
+ });
275
332
  }
276
333
  /* ===== Helpers consumed by the router ===== */
277
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;
@@ -35,12 +39,12 @@ function startSchemaService(server, opts = {}) {
35
39
  nsp.on('connection', (socket) => {
36
40
  Logger_1.default.info('SchemaService:', socket.id);
37
41
  // Send normalized (secure) snapshot on connect
38
- socket.emit('schemasData', loader.getPublicSchemaList());
42
+ socket.emit('schemasData', loader.getPublicSchemaList(true));
39
43
  });
40
44
  // Broadcast sanitized updates on hot-reload / schema changes
41
45
  if (typeof loader.on === 'function') {
42
46
  loader.on('schemasChanged', () => {
43
- nsp?.emit('schemasUpdated', loader.getPublicSchemaList());
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 {};