@hotfusion/modeller 0.0.7 → 0.0.9
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 +784 -38
- package/dist/View.js +3 -0
- package/dist/View.js.map +1 -0
- package/dist/connector.js +1 -0
- package/dist/connector.js.map +1 -1
- package/dist/extensions/oidc/client.js +221 -0
- package/dist/extensions/oidc/client.js.map +1 -0
- package/dist/extensions/oidc/index.js +192 -0
- package/dist/extensions/oidc/index.js.map +1 -0
- package/dist/logger.js +129 -0
- package/dist/logger.js.map +1 -0
- package/dist/model.js +112 -96
- package/dist/model.js.map +1 -1
- package/dist/server.js +333 -183
- package/dist/server.js.map +1 -1
- package/dist/types/View.d.ts +2 -0
- package/dist/types/View.d.ts.map +1 -0
- package/dist/types/connector.d.ts +1 -1
- package/dist/types/connector.d.ts.map +1 -1
- package/dist/types/extensions/oidc/client.d.ts +32 -0
- package/dist/types/extensions/oidc/client.d.ts.map +1 -0
- package/dist/types/extensions/oidc/index.d.ts +20 -0
- package/dist/types/extensions/oidc/index.d.ts.map +1 -0
- package/dist/types/extensions/oidc/oidc.d.ts +20 -0
- package/dist/types/extensions/oidc/oidc.d.ts.map +1 -0
- package/dist/types/extensions/oidc.d.ts +20 -0
- package/dist/types/extensions/oidc.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/logger.d.ts +24 -0
- package/dist/types/logger.d.ts.map +1 -0
- package/dist/types/model.d.ts +23 -11
- package/dist/types/model.d.ts.map +1 -1
- package/dist/types/server.d.ts +23 -4
- package/dist/types/server.d.ts.map +1 -1
- package/dist/types/utils/bundler.d.ts +2 -0
- package/dist/types/utils/bundler.d.ts.map +1 -1
- package/dist/types/view.d.ts +13 -0
- package/dist/types/view.d.ts.map +1 -0
- package/dist/utils/bundler.js +138 -39
- package/dist/utils/bundler.js.map +1 -1
- package/dist/view.js +34 -0
- package/dist/view.js.map +1 -0
- package/package.json +16 -7
package/dist/server.js
CHANGED
|
@@ -14,14 +14,11 @@ const eventemitter3_1 = require("eventemitter3");
|
|
|
14
14
|
const jose_1 = require("jose");
|
|
15
15
|
const socket_io_1 = require("socket.io");
|
|
16
16
|
const keygen_js_1 = require("./utils/keygen.js");
|
|
17
|
+
const koa_static_1 = __importDefault(require("koa-static"));
|
|
17
18
|
class Utils {
|
|
18
19
|
static trimPath(path) {
|
|
19
20
|
return '/' + path.split('/').filter((x) => x).join('/');
|
|
20
21
|
}
|
|
21
|
-
static postman(Event) {
|
|
22
|
-
if (Event.middleware === 'postman')
|
|
23
|
-
return Event.ctx.redirect(`/?access_token=${Event.profile.accessToken}&clientId=${Event.provider.clientId}&providerId=${Event.provider.id}&scopes=${Event.provider.scopes.join(' ')}`);
|
|
24
|
-
}
|
|
25
22
|
}
|
|
26
23
|
class Server extends eventemitter3_1.EventEmitter {
|
|
27
24
|
port;
|
|
@@ -31,6 +28,18 @@ class Server extends eventemitter3_1.EventEmitter {
|
|
|
31
28
|
providers = [];
|
|
32
29
|
config;
|
|
33
30
|
io;
|
|
31
|
+
registeredModels = [];
|
|
32
|
+
extensions = [];
|
|
33
|
+
extensionConfigs = new Map();
|
|
34
|
+
API_OPERATIONS = ['insert', 'update', 'delete', 'get', 'find', 'list'];
|
|
35
|
+
OPERATION_METHODS = {
|
|
36
|
+
insert: 'POST',
|
|
37
|
+
update: 'PUT',
|
|
38
|
+
delete: 'DELETE',
|
|
39
|
+
get: 'GET',
|
|
40
|
+
find: 'POST',
|
|
41
|
+
list: 'GET',
|
|
42
|
+
};
|
|
34
43
|
constructor(port, config) {
|
|
35
44
|
super();
|
|
36
45
|
this.app = new koa_1.default();
|
|
@@ -52,125 +61,173 @@ class Server extends eventemitter3_1.EventEmitter {
|
|
|
52
61
|
this.port = port;
|
|
53
62
|
this.config = config;
|
|
54
63
|
}
|
|
55
|
-
//
|
|
64
|
+
// ?? Register model ??????????????????????????????????????????????????????????
|
|
65
|
+
registerModel(id, scope, model) {
|
|
66
|
+
this.registeredModels.push({ id, scope, model });
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
// ?? Register extension ??????????????????????????????????????????????????????
|
|
70
|
+
registerExtension(extension) {
|
|
71
|
+
this.extensions.push(extension);
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
// ?? Setup extensions ??????????????????????????????????????????????????????
|
|
75
|
+
async setupExtensions() {
|
|
76
|
+
console.log('[Server] Setting up extensions...');
|
|
77
|
+
for (const ext of this.extensions) {
|
|
78
|
+
console.log(`[Server] Setting up extension: ${ext.id}`);
|
|
79
|
+
const extConfig = this.extensionConfigs.get(ext.id) || {};
|
|
80
|
+
console.log(`[Server] Extension ${ext.id} config:`, extConfig);
|
|
81
|
+
await ext.setup(this, extConfig);
|
|
82
|
+
console.log(`[Server] Extension ${ext.id} setup complete`);
|
|
83
|
+
}
|
|
84
|
+
console.log('[Server] All extensions setup complete');
|
|
85
|
+
}
|
|
86
|
+
// ?? Auto-mount API routes ?????????????????????????????????????????????????????????
|
|
87
|
+
mountModelRoutes() {
|
|
88
|
+
for (const { id, scope, model } of this.registeredModels) {
|
|
89
|
+
this._mountModelRecursive(id, scope, model);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
_mountModelRecursive(id, scope, model) {
|
|
93
|
+
this._mountModel(id, scope, model);
|
|
94
|
+
for (const ext of model.getExtensions?.() ?? []) {
|
|
95
|
+
this._mountModelRecursive(id, `${scope}/${ext.id}`, ext);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
_mountModel(id, scope, model) {
|
|
99
|
+
// ?? CRUD operations ???????????????????????????????????????????????????
|
|
100
|
+
for (const operation of this.API_OPERATIONS) {
|
|
101
|
+
const routePath = `/${id}/${scope}/${operation}`;
|
|
102
|
+
const handler = async (ctx) => {
|
|
103
|
+
const { private: _priv, ...body } = { ...ctx.query, ...(ctx.request.body ?? {}) };
|
|
104
|
+
try {
|
|
105
|
+
let result;
|
|
106
|
+
if (operation === 'insert') {
|
|
107
|
+
result = await model.insert(body, { private: true });
|
|
108
|
+
}
|
|
109
|
+
else if (operation === 'update') {
|
|
110
|
+
const { key, data } = body;
|
|
111
|
+
result = await model.update(key, data, { private: true });
|
|
112
|
+
}
|
|
113
|
+
else if (operation === 'delete') {
|
|
114
|
+
const query = Object.keys(ctx.request.body ?? {}).length ? ctx.request.body : ctx.query;
|
|
115
|
+
result = await model.delete(query);
|
|
116
|
+
}
|
|
117
|
+
else if (operation === 'get') {
|
|
118
|
+
result = await model.get(body, { private: true });
|
|
119
|
+
}
|
|
120
|
+
else if (operation === 'list') {
|
|
121
|
+
const { private: _p, start, count, ...query } = body;
|
|
122
|
+
result = await model.list(query, { start: Number(start) || 0, count: Number(count) || 10000, private: true });
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const { private: _p, max, ...query } = body;
|
|
126
|
+
result = await model.find(query, { max, private: true });
|
|
127
|
+
}
|
|
128
|
+
ctx.status = 200;
|
|
129
|
+
ctx.body = { ok: true, entry: result };
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
ctx.status = err?.status ?? 500;
|
|
133
|
+
ctx.body = { ok: false, error: err?.message ?? err };
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
if (operation === 'insert')
|
|
137
|
+
this.router.post(routePath, handler);
|
|
138
|
+
else if (operation === 'update')
|
|
139
|
+
this.router.put(routePath, handler);
|
|
140
|
+
else if (operation === 'delete')
|
|
141
|
+
this.router.delete(routePath, handler);
|
|
142
|
+
else if (operation === 'get')
|
|
143
|
+
this.router.get(routePath, handler);
|
|
144
|
+
else if (operation === 'find')
|
|
145
|
+
this.router.post(routePath, handler);
|
|
146
|
+
else if (operation === 'list')
|
|
147
|
+
this.router.get(routePath, handler);
|
|
148
|
+
}
|
|
149
|
+
// ?? Custom methods ????????????????????????????????????????????????????
|
|
150
|
+
for (const method of model.getMethods?.() ?? []) {
|
|
151
|
+
this.router.post(`/${id}/${scope}/${method}`, async (ctx) => {
|
|
152
|
+
const { _id, ...rest } = ctx.request.body ?? {};
|
|
153
|
+
try {
|
|
154
|
+
const result = await model.call(method, { _id, ...rest });
|
|
155
|
+
ctx.status = 200;
|
|
156
|
+
ctx.body = { ok: true, entry: result };
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
ctx.status = err?.status ?? 500;
|
|
160
|
+
ctx.body = { ok: false, error: err?.message ?? err };
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// ?? Upload methods (multipart HTTP) ???????????????????????????????????
|
|
165
|
+
for (const uploadId of model.getUploads?.() ?? []) {
|
|
166
|
+
this.upload(`/${id}/${scope}/${uploadId}`, async (ctx, file, fields) => {
|
|
167
|
+
try {
|
|
168
|
+
return await model.callUpload(uploadId, file, fields);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
// ?? View routes ??????????????????????????????????????????????????????????????????????
|
|
176
|
+
for (const view of model.getViews?.() ?? []) {
|
|
177
|
+
const routePath = `/${id}/${scope}${view.path}`.replace(/\/$/, '') || '/';
|
|
178
|
+
console.log('[View] Mounting route:', routePath);
|
|
179
|
+
this.router.get(routePath, async (ctx) => {
|
|
180
|
+
try {
|
|
181
|
+
ctx.type = 'text/html';
|
|
182
|
+
ctx.body = await view.render(ctx.query);
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
ctx.status = err?.status ?? 500;
|
|
186
|
+
ctx.body = { ok: false, error: err?.message ?? err };
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
// ?? Static folders ????????????????????????????????????????????????????
|
|
191
|
+
for (const [urlPath, { path: folderPath }] of model.getFolders?.() ?? new Map()) {
|
|
192
|
+
const staticMiddleware = (0, koa_static_1.default)(folderPath);
|
|
193
|
+
this.app.use(async (ctx, next) => {
|
|
194
|
+
if (ctx.path.startsWith(urlPath)) {
|
|
195
|
+
ctx.path = ctx.path.slice(urlPath.length) || '/';
|
|
196
|
+
return staticMiddleware(ctx, next);
|
|
197
|
+
}
|
|
198
|
+
return next();
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// ?? Stream methods (binary socket) ????????????????????????????????????
|
|
202
|
+
for (const streamId of model.getStreams?.() ?? []) {
|
|
203
|
+
// Streams are registered globally in _setupStream
|
|
204
|
+
// Store them for later reference
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// ?? OIDC Providers ?????????????????????????????????????????????????????
|
|
208
|
+
authPath;
|
|
56
209
|
setOIDCProviders(path, providers) {
|
|
210
|
+
console.log('[Server] setOIDCProviders called with path:', path, 'providers:', providers?.map(p => p.id));
|
|
211
|
+
this.authPath = path;
|
|
57
212
|
if (this.config.domain)
|
|
58
213
|
path = `${this.config.domain}/${path}`;
|
|
59
|
-
path
|
|
60
|
-
= path.split('/').filter((x) => x).join('/');
|
|
214
|
+
path = path.split('/').filter((x) => x).join('/');
|
|
61
215
|
providers?.forEach?.((provider) => {
|
|
62
216
|
if (!this.providers.find(x => x.clientId === provider.clientId))
|
|
63
217
|
this.providers.push({ ...provider });
|
|
64
218
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (ctx.params.provider === 'callback' || !ctx.path.includes('/logout'))
|
|
71
|
-
return await next();
|
|
72
|
-
let provider = this.providers.find(x => x.id === ctx.params.provider);
|
|
73
|
-
if (provider) {
|
|
74
|
-
let SESSION = ctx.headers.authorization || '';
|
|
75
|
-
if (!SESSION)
|
|
76
|
-
ctx.throw(401, 'Unauthorized');
|
|
77
|
-
if (SESSION) {
|
|
78
|
-
SESSION = SESSION.split(' ').pop() || '';
|
|
79
|
-
let token;
|
|
80
|
-
try {
|
|
81
|
-
token = (await (0, jose_1.jwtVerify)(SESSION, new TextEncoder().encode(Array.isArray(this.app?.keys) ? this.app.keys[0] : undefined))).payload;
|
|
82
|
-
}
|
|
83
|
-
catch (e) {
|
|
84
|
-
token = (0, jose_1.decodeJwt)(SESSION);
|
|
85
|
-
}
|
|
86
|
-
let body = {};
|
|
87
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
88
|
-
ctx.body = body;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
/*this.routes.push({
|
|
94
|
-
method: 'get',
|
|
95
|
-
path: Utils.trimPath(path) + '/:provider',
|
|
96
|
-
callback: async (ctx, next) => {
|
|
97
|
-
if (ctx.params.provider === 'callback')
|
|
98
|
-
return await next();
|
|
99
|
-
|
|
100
|
-
let provider:IOIDCProvider | undefined
|
|
101
|
-
= this.providers.find(x => x.id === ctx.params.provider);
|
|
102
|
-
|
|
103
|
-
if (provider) {
|
|
104
|
-
try{
|
|
105
|
-
let {url, code_verifier, state}
|
|
106
|
-
= await new OIDCClient(provider as any ).buildURL(provider.scopes.join(' '));
|
|
107
|
-
|
|
108
|
-
let {middleware} = ctx.request.query || {};
|
|
109
|
-
ctx.session!.oidc = {
|
|
110
|
-
__SID : ctx.query.__SID,
|
|
111
|
-
provider,
|
|
112
|
-
code_verifier,
|
|
113
|
-
state,
|
|
114
|
-
middleware: middleware || false,
|
|
115
|
-
query : (ctx.request.querystring || '').replace(/^\?/, '')
|
|
116
|
-
.split('&')
|
|
117
|
-
.filter(Boolean)
|
|
118
|
-
.reduce<Record<string, string>>((acc, pair) => {
|
|
119
|
-
const [key, value = ''] = pair.split('=');
|
|
120
|
-
acc[decodeURIComponent(key as any )] = decodeURIComponent(value);
|
|
121
|
-
return acc;
|
|
122
|
-
}, {})
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
await ctx.session!.save?.();
|
|
126
|
-
ctx.redirect(url.toString());
|
|
127
|
-
}catch (e:any){
|
|
128
|
-
console.error(e);
|
|
129
|
-
ctx.body = e.message;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
})*/
|
|
134
|
-
/*this.routes.push({
|
|
135
|
-
method : 'get',
|
|
136
|
-
path : Utils.trimPath(path.replace(this.config.domain + '/', '') + '/callback'),
|
|
137
|
-
callback : async (ctx) => {
|
|
138
|
-
let origin
|
|
139
|
-
= ctx.session?.oidc?.query?.origin || '';
|
|
140
|
-
|
|
141
|
-
const session
|
|
142
|
-
= ctx.session?.oidc;
|
|
143
|
-
|
|
144
|
-
const code
|
|
145
|
-
= ctx.query.code as string;
|
|
146
|
-
|
|
147
|
-
const oidcClient
|
|
148
|
-
= new OIDCClient(session.provider);
|
|
149
|
-
|
|
150
|
-
const TokenCollection
|
|
151
|
-
= await oidcClient.getToken(code,session.code_verifier)
|
|
152
|
-
|
|
153
|
-
this.emit('provider',{token:TokenCollection,sid:session.__SID,session});
|
|
154
|
-
ctx.type = 'html';
|
|
155
|
-
ctx.body = `<script>window?.close?.()</script>`
|
|
156
|
-
}
|
|
157
|
-
})*/
|
|
158
|
-
}
|
|
219
|
+
// Store OIDC config for extension
|
|
220
|
+
this.extensionConfigs.set('oidc', {
|
|
221
|
+
providers: providers || [],
|
|
222
|
+
authPath: path,
|
|
223
|
+
});
|
|
159
224
|
return this;
|
|
160
225
|
}
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
// Registers a multipart/form-data route. The handler receives the parsed
|
|
164
|
-
// file as a Buffer along with any other form fields.
|
|
165
|
-
//
|
|
166
|
-
// Example:
|
|
167
|
-
// server.upload('/system/filesystem/upload', async (ctx, file, fields) => {
|
|
168
|
-
// return { ok: true };
|
|
169
|
-
// });
|
|
226
|
+
// ?? Upload ????????????????????????????????????????????????????????????????
|
|
170
227
|
upload(path, handler) {
|
|
171
228
|
this.routes.push({
|
|
172
229
|
method: 'post',
|
|
173
|
-
path,
|
|
230
|
+
path: Utils.trimPath(path),
|
|
174
231
|
callback: async (ctx) => {
|
|
175
232
|
return new Promise((resolve, reject) => {
|
|
176
233
|
const fields = {};
|
|
@@ -204,17 +261,7 @@ class Server extends eventemitter3_1.EventEmitter {
|
|
|
204
261
|
});
|
|
205
262
|
return this;
|
|
206
263
|
}
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
// Registers socket handlers for chunked binary file streaming with resume.
|
|
210
|
-
// The handler receives the complete reassembled Buffer and the upload metadata.
|
|
211
|
-
// Call this once — it handles all uploads for any model/scope.
|
|
212
|
-
//
|
|
213
|
-
// Example:
|
|
214
|
-
// server.stream(async (buffer, meta) => {
|
|
215
|
-
// // meta = { _id, path, id, scope, ... }
|
|
216
|
-
// return { ok: true };
|
|
217
|
-
// });
|
|
264
|
+
// ?? Stream ????????????????????????????????????????????????????????????????
|
|
218
265
|
_streamHandler;
|
|
219
266
|
_uploadTmp = '/tmp/hotfusion-uploads';
|
|
220
267
|
stream(handler) {
|
|
@@ -224,9 +271,7 @@ class Server extends eventemitter3_1.EventEmitter {
|
|
|
224
271
|
_setupStream(socket) {
|
|
225
272
|
const handler = this._streamHandler;
|
|
226
273
|
const uploadTmp = this._uploadTmp;
|
|
227
|
-
// uploadId → { meta, totalChunks, received }
|
|
228
274
|
const uploads = new Map();
|
|
229
|
-
// Check disk for existing chunks (resume support)
|
|
230
275
|
socket.on('upload:resume', async ({ uploadId }) => {
|
|
231
276
|
const uploadDir = `${uploadTmp}/${uploadId}`;
|
|
232
277
|
let fromChunk = 0;
|
|
@@ -240,10 +285,8 @@ class Server extends eventemitter3_1.EventEmitter {
|
|
|
240
285
|
catch { }
|
|
241
286
|
socket.emit(`upload:resume:${uploadId}`, { fromChunk });
|
|
242
287
|
});
|
|
243
|
-
// Client announces upload metadata — make this synchronous so chunks can't race it
|
|
244
288
|
socket.on('upload:start', ({ uploadId, id, scope, method, meta, totalChunks }) => {
|
|
245
289
|
const fullMeta = { ...meta, id, scope, method };
|
|
246
|
-
// Synchronous mkdir — no awaits before uploads.set()
|
|
247
290
|
const fs = require('node:fs');
|
|
248
291
|
fs.mkdirSync(`${uploadTmp}/${uploadId}`, { recursive: true });
|
|
249
292
|
const received = new Set();
|
|
@@ -254,10 +297,8 @@ class Server extends eventemitter3_1.EventEmitter {
|
|
|
254
297
|
}
|
|
255
298
|
catch { }
|
|
256
299
|
uploads.set(uploadId, { meta: fullMeta, totalChunks, received });
|
|
257
|
-
// Ack so client knows it's safe to send chunks
|
|
258
300
|
socket.emit(`upload:started:${uploadId}`);
|
|
259
301
|
});
|
|
260
|
-
// Client sends a raw binary chunk
|
|
261
302
|
socket.on('upload:chunk', async ({ uploadId, index, data }) => {
|
|
262
303
|
const upload = uploads.get(uploadId);
|
|
263
304
|
if (!upload) {
|
|
@@ -292,70 +333,179 @@ class Server extends eventemitter3_1.EventEmitter {
|
|
|
292
333
|
}
|
|
293
334
|
});
|
|
294
335
|
}
|
|
336
|
+
// ?? Socket subscriptions ????????????????????????????????????????????????????
|
|
337
|
+
_setupSubscriptions(socket) {
|
|
338
|
+
const socketSubs = new Map();
|
|
339
|
+
const resolveModel = (id, scope) => {
|
|
340
|
+
const [baseScope, ext] = scope.split('.');
|
|
341
|
+
const entry = this.registeredModels.find(m => m.id === id && m.scope === baseScope);
|
|
342
|
+
return ext ? entry?.model[ext] : entry?.model;
|
|
343
|
+
};
|
|
344
|
+
socket.on('subscribe', ({ id, scope, query, reqId }) => {
|
|
345
|
+
const model = resolveModel(id, scope);
|
|
346
|
+
if (!model) {
|
|
347
|
+
socket.emit('error', { code: 'MODEL_NOT_FOUND', id, scope });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const { subscription, subId } = model.subscribe(query);
|
|
351
|
+
const channel = [id, scope, subId].join('.');
|
|
352
|
+
const existing = socketSubs.get(subId);
|
|
353
|
+
if (existing)
|
|
354
|
+
existing.subscription.off('publish', existing.listener);
|
|
355
|
+
const listener = (payload) => socket.emit(channel, payload);
|
|
356
|
+
subscription.on('publish', listener);
|
|
357
|
+
socketSubs.set(subId, { model, subscription, listener });
|
|
358
|
+
socket.emit('subscribed', { id, scope, query, subId, events: subscription.getEvents(), reqId });
|
|
359
|
+
});
|
|
360
|
+
socket.on('event:call', async ({ id, scope, query, event, args, callId }) => {
|
|
361
|
+
const model = resolveModel(id, scope);
|
|
362
|
+
if (!model) {
|
|
363
|
+
socket.emit('event:result', { callId, error: { code: 'MODEL_NOT_FOUND' } });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
const { subscription } = model.subscribe(query);
|
|
368
|
+
const result = await subscription.dispatch(event, args, { socket, scope });
|
|
369
|
+
socket.emit('event:result', { callId, result });
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
socket.emit('event:result', { callId, error });
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
socket.on('unsubscribe', ({ id, scope, query }) => {
|
|
376
|
+
const model = resolveModel(id, scope);
|
|
377
|
+
if (!model)
|
|
378
|
+
return;
|
|
379
|
+
const subId = JSON.stringify(Object.fromEntries(Object.entries(query).sort()));
|
|
380
|
+
const existing = socketSubs.get(subId);
|
|
381
|
+
if (existing) {
|
|
382
|
+
existing.subscription.off('publish', existing.listener);
|
|
383
|
+
socketSubs.delete(subId);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
socket.on('disconnect', () => {
|
|
387
|
+
for (const { subscription, listener } of socketSubs.values()) {
|
|
388
|
+
subscription.off('publish', listener);
|
|
389
|
+
}
|
|
390
|
+
socketSubs.clear();
|
|
391
|
+
this.registeredModels.forEach(m => m.model.emit('disconnect', socket));
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
// ?? Metadata endpoint ??????????????????????????????????????????????????????
|
|
395
|
+
_setupMetadataRoute() {
|
|
396
|
+
this.router.get('/@metadata', async (ctx) => {
|
|
397
|
+
const buildMeta = (id, scope, model) => ({
|
|
398
|
+
id,
|
|
399
|
+
scope,
|
|
400
|
+
schema: model.getSchema?.(),
|
|
401
|
+
schemes: model.getSchemes?.(),
|
|
402
|
+
operations: this.API_OPERATIONS.map(op => ({
|
|
403
|
+
operation: op,
|
|
404
|
+
method: this.OPERATION_METHODS[op],
|
|
405
|
+
path: `/${id}/${scope}/${op}`,
|
|
406
|
+
url: `http://localhost:${this.port}/${id}/${scope}/${op}`,
|
|
407
|
+
schema: op === 'insert' || op === 'update' ? model.getSchema?.() : { type: 'object', properties: { _id: { type: 'string' } } },
|
|
408
|
+
})),
|
|
409
|
+
methods: (model.getMethods?.() ?? []).map((m) => ({
|
|
410
|
+
method: m,
|
|
411
|
+
path: `/${id}/${scope}/${m}`,
|
|
412
|
+
url: `http://localhost:${this.port}/${id}/${scope}/${m}`,
|
|
413
|
+
})),
|
|
414
|
+
extensions: (model.getExtensions?.() ?? []).map((ext) => buildMeta(id, `${scope}/${ext.id}`, ext)),
|
|
415
|
+
});
|
|
416
|
+
ctx.body = { api: this.registeredModels.map(m => buildMeta(m.id, m.scope, m.model)) };
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
// ?? Route helper (kept for backward compatibility) ??????????????????????????
|
|
295
420
|
route(route) {
|
|
296
421
|
route.path = Utils.trimPath(route.path);
|
|
297
422
|
this.routes.push(route);
|
|
298
423
|
return this;
|
|
299
424
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
425
|
+
// ?? Start server ????????????????????????????????????????????????????????????
|
|
426
|
+
async start() {
|
|
427
|
+
console.log('[Server] Start called');
|
|
428
|
+
// Wait a bit then start the server setup
|
|
429
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
430
|
+
console.log('[Server] Setting up app');
|
|
431
|
+
// Error handling middleware
|
|
432
|
+
this.app.use(async (ctx, next) => {
|
|
433
|
+
try {
|
|
434
|
+
await next();
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
ctx.status = err.status || 500;
|
|
438
|
+
ctx.body = {
|
|
439
|
+
error: err.message || 'Internal error',
|
|
440
|
+
status: err.status
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
// Mount auto-generated model routes
|
|
445
|
+
this.mountModelRoutes();
|
|
446
|
+
// Setup extensions FIRST (before any routes)
|
|
447
|
+
console.log('[Server] Before setupExtensions');
|
|
448
|
+
await this.setupExtensions();
|
|
449
|
+
console.log('[Server] After setupExtensions');
|
|
450
|
+
// Mount metadata endpoint
|
|
451
|
+
this._setupMetadataRoute();
|
|
452
|
+
// Mount manually registered routes
|
|
453
|
+
for (let i = 0; i < this.routes.length; i++) {
|
|
454
|
+
let { method, path, callback } = this.routes[i] || {};
|
|
455
|
+
if (method && path && callback) {
|
|
456
|
+
const handler = async (ctx, next) => {
|
|
457
|
+
let flags = {
|
|
458
|
+
protected: this.routes?.[i]?.protected || false
|
|
311
459
|
};
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
let flags = {
|
|
319
|
-
protected: this.routes?.[i]?.protected || false
|
|
320
|
-
};
|
|
321
|
-
try {
|
|
322
|
-
if (flags.protected) {
|
|
323
|
-
let TOKEN = ctx.headers.authorization;
|
|
324
|
-
if (!TOKEN)
|
|
325
|
-
ctx.throw(401, 'Unauthorized');
|
|
326
|
-
ctx.token
|
|
327
|
-
= (await (0, jose_1.jwtVerify)(TOKEN.split(' ').pop(), new TextEncoder().encode(Array.isArray(this.app?.keys) ? this.app.keys[0] : undefined))).payload;
|
|
328
|
-
}
|
|
329
|
-
let body = await callback(ctx, next);
|
|
330
|
-
if (!ctx.body) {
|
|
331
|
-
ctx.status = 200;
|
|
332
|
-
ctx.body = body;
|
|
333
|
-
}
|
|
460
|
+
try {
|
|
461
|
+
if (flags.protected) {
|
|
462
|
+
let TOKEN = ctx.headers.authorization;
|
|
463
|
+
if (!TOKEN)
|
|
464
|
+
ctx.throw(401, 'Unauthorized');
|
|
465
|
+
ctx.token = (await (0, jose_1.jwtVerify)(TOKEN.split(' ').pop(), new TextEncoder().encode(Array.isArray(this.app?.keys) ? this.app.keys[0] : undefined))).payload;
|
|
334
466
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
ctx.
|
|
338
|
-
|
|
339
|
-
};
|
|
467
|
+
let body = await callback(ctx, next);
|
|
468
|
+
if (!ctx.body) {
|
|
469
|
+
ctx.status = 200;
|
|
470
|
+
ctx.body = body;
|
|
340
471
|
}
|
|
341
|
-
}
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
ctx.status = 500;
|
|
475
|
+
ctx.body = {
|
|
476
|
+
error: err.message || err.code || err.details || 'Internal error',
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
if (method === 'get')
|
|
481
|
+
this.router.get(path, handler);
|
|
482
|
+
else if (method === 'post')
|
|
483
|
+
this.router.post(path, handler);
|
|
484
|
+
else if (method === 'put')
|
|
485
|
+
this.router.put(path, handler);
|
|
486
|
+
else if (method === 'delete')
|
|
487
|
+
this.router.delete(path, handler);
|
|
488
|
+
else if (method === 'patch')
|
|
489
|
+
this.router.patch(path, handler);
|
|
342
490
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
491
|
+
}
|
|
492
|
+
this.app.use(this.router.routes());
|
|
493
|
+
this.app.use(this.router.allowedMethods());
|
|
494
|
+
const server = this.app.listen(this.port, '0.0.0.0', () => {
|
|
495
|
+
this.emit("mounted", this);
|
|
496
|
+
});
|
|
497
|
+
this.io = new socket_io_1.Server(server, {
|
|
498
|
+
cors: { origin: "*", methods: ["GET", "POST"] },
|
|
499
|
+
});
|
|
500
|
+
this.io.use((socket, next) => {
|
|
501
|
+
next();
|
|
502
|
+
});
|
|
503
|
+
this.io.on("connection", (socket) => {
|
|
504
|
+
this._setupStream(socket);
|
|
505
|
+
this._setupSubscriptions(socket);
|
|
506
|
+
this.emit('connection', { socket });
|
|
507
|
+
});
|
|
508
|
+
console.log('[Server] Start complete, listening on port', this.port);
|
|
359
509
|
return this;
|
|
360
510
|
}
|
|
361
511
|
}
|