@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.
- package/README.md +48 -5
- package/dist/api/apiRouter.d.ts +2 -0
- package/dist/api/apiRouter.js +67 -21
- package/dist/api/router/mountCrudRoutes.js +207 -220
- 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/utils.js +20 -7
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +83 -0
- package/dist/database/DatabaseAdapter.d.ts +7 -0
- package/dist/database/NMAdapter.d.ts +41 -0
- package/dist/database/NMAdapter.js +979 -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/index.d.ts +4 -1
- package/dist/index.js +11 -5
- package/dist/models/BaseModel.d.ts +16 -0
- package/dist/models/BaseModel.js +32 -4
- package/dist/policy/authorize.js +95 -38
- package/dist/schemas/Users.json +66 -30
- 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.js +12 -10
- package/dist/utils/QueryCache.d.ts +16 -0
- package/dist/utils/QueryCache.js +106 -0
- package/dist/utils/SchemaLoader.d.ts +5 -0
- package/dist/utils/SchemaLoader.js +45 -3
- 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/dist/policy/authorize.js
CHANGED
|
@@ -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
|
-
|
|
104
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/schemas/Users.json
CHANGED
|
@@ -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": [
|
|
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": [
|
|
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":
|
|
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": {
|
|
107
|
+
"authenticated": {
|
|
108
|
+
"id": {
|
|
109
|
+
"ne": "$CTX.userId"
|
|
110
|
+
}
|
|
111
|
+
},
|
|
101
112
|
"roles": {
|
|
102
|
-
"admin":
|
|
103
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
"writeDeny": [
|
|
110
|
-
|
|
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": {
|
|
114
|
-
|
|
115
|
-
|
|
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": {
|
|
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
|
-
|
|
239
|
-
|
|
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 {};
|