@objectstack/runtime 0.9.2 → 1.0.0
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/CHANGELOG.md +16 -0
- package/dist/api-registry-plugin.d.ts +16 -0
- package/dist/api-registry-plugin.js +42 -0
- package/dist/app-plugin.d.ts +2 -2
- package/dist/app-plugin.js +61 -61
- package/dist/app-plugin.test.d.ts +1 -0
- package/dist/app-plugin.test.js +80 -0
- package/dist/driver-plugin.d.ts +2 -2
- package/dist/driver-plugin.js +14 -14
- package/dist/http-dispatcher.d.ts +106 -0
- package/dist/http-dispatcher.js +515 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/runtime.d.ts +45 -0
- package/dist/runtime.js +50 -0
- package/dist/runtime.test.d.ts +1 -0
- package/dist/runtime.test.js +57 -0
- package/package.json +9 -6
- package/src/api-registry-plugin.ts +58 -0
- package/src/app-plugin.test.ts +102 -0
- package/src/app-plugin.ts +2 -2
- package/src/driver-plugin.ts +2 -2
- package/src/http-dispatcher.ts +600 -0
- package/src/index.ts +8 -0
- package/src/runtime.test.ts +65 -0
- package/src/runtime.ts +78 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import { CoreServiceName } from '@objectstack/spec/system';
|
|
2
|
+
export class HttpDispatcher {
|
|
3
|
+
constructor(kernel) {
|
|
4
|
+
this.kernel = kernel;
|
|
5
|
+
}
|
|
6
|
+
success(data, meta) {
|
|
7
|
+
return {
|
|
8
|
+
status: 200,
|
|
9
|
+
body: { success: true, data, meta }
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
error(message, code = 500, details) {
|
|
13
|
+
return {
|
|
14
|
+
status: code,
|
|
15
|
+
body: { success: false, error: { message, code, details } }
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
ensureBroker() {
|
|
19
|
+
if (!this.kernel.broker) {
|
|
20
|
+
throw { statusCode: 500, message: 'Kernel Broker not available' };
|
|
21
|
+
}
|
|
22
|
+
return this.kernel.broker;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Generates the discovery JSON response for the API root
|
|
26
|
+
*/
|
|
27
|
+
getDiscoveryInfo(prefix) {
|
|
28
|
+
const services = this.getServicesMap();
|
|
29
|
+
const hasGraphQL = !!(services[CoreServiceName.enum.graphql] || this.kernel.graphql);
|
|
30
|
+
const hasSearch = !!services[CoreServiceName.enum.search];
|
|
31
|
+
const hasWebSockets = !!services[CoreServiceName.enum.realtime];
|
|
32
|
+
const hasFiles = !!(services[CoreServiceName.enum['file-storage']] || services['storage']?.supportsFiles);
|
|
33
|
+
const hasAnalytics = !!services[CoreServiceName.enum.analytics];
|
|
34
|
+
const hasHub = !!services[CoreServiceName.enum.hub];
|
|
35
|
+
return {
|
|
36
|
+
name: 'ObjectOS',
|
|
37
|
+
version: '1.0.0',
|
|
38
|
+
environment: process.env.NODE_ENV || 'development',
|
|
39
|
+
routes: {
|
|
40
|
+
data: `${prefix}/data`,
|
|
41
|
+
metadata: `${prefix}/metadata`,
|
|
42
|
+
auth: `${prefix}/auth`,
|
|
43
|
+
graphql: hasGraphQL ? `${prefix}/graphql` : undefined,
|
|
44
|
+
storage: hasFiles ? `${prefix}/storage` : undefined,
|
|
45
|
+
analytics: hasAnalytics ? `${prefix}/analytics` : undefined,
|
|
46
|
+
hub: hasHub ? `${prefix}/hub` : undefined,
|
|
47
|
+
},
|
|
48
|
+
features: {
|
|
49
|
+
graphql: hasGraphQL,
|
|
50
|
+
search: hasSearch,
|
|
51
|
+
websockets: hasWebSockets,
|
|
52
|
+
files: hasFiles,
|
|
53
|
+
analytics: hasAnalytics,
|
|
54
|
+
hub: hasHub,
|
|
55
|
+
},
|
|
56
|
+
locale: {
|
|
57
|
+
default: 'en',
|
|
58
|
+
supported: ['en', 'zh-CN'],
|
|
59
|
+
timezone: 'UTC'
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Handles GraphQL requests
|
|
65
|
+
*/
|
|
66
|
+
async handleGraphQL(body, context) {
|
|
67
|
+
if (!body || !body.query) {
|
|
68
|
+
throw { statusCode: 400, message: 'Missing query in request body' };
|
|
69
|
+
}
|
|
70
|
+
if (typeof this.kernel.graphql !== 'function') {
|
|
71
|
+
throw { statusCode: 501, message: 'GraphQL service not available' };
|
|
72
|
+
}
|
|
73
|
+
return this.kernel.graphql(body.query, body.variables, {
|
|
74
|
+
request: context.request
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Handles Auth requests
|
|
79
|
+
* path: sub-path after /auth/
|
|
80
|
+
*/
|
|
81
|
+
async handleAuth(path, method, body, context) {
|
|
82
|
+
// 1. Try generic Auth Service
|
|
83
|
+
const authService = this.getService(CoreServiceName.enum.auth);
|
|
84
|
+
if (authService && typeof authService.handler === 'function') {
|
|
85
|
+
const response = await authService.handler(context.request, context.response);
|
|
86
|
+
return { handled: true, result: response };
|
|
87
|
+
}
|
|
88
|
+
// 2. Legacy Login
|
|
89
|
+
const normalizedPath = path.replace(/^\/+/, '');
|
|
90
|
+
if (normalizedPath === 'login' && method.toUpperCase() === 'POST') {
|
|
91
|
+
const broker = this.ensureBroker();
|
|
92
|
+
const data = await broker.call('auth.login', body, { request: context.request });
|
|
93
|
+
return { handled: true, response: { status: 200, body: data } };
|
|
94
|
+
}
|
|
95
|
+
return { handled: false };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Handles Metadata requests
|
|
99
|
+
* Standard: /metadata/:type/:name
|
|
100
|
+
* Fallback for backward compat: /metadata (all objects), /metadata/:objectName (get object)
|
|
101
|
+
*/
|
|
102
|
+
async handleMetadata(path, context) {
|
|
103
|
+
const broker = this.ensureBroker();
|
|
104
|
+
const parts = path.replace(/^\/+/, '').split('/').filter(Boolean);
|
|
105
|
+
// GET /metadata/types
|
|
106
|
+
if (parts[0] === 'types') {
|
|
107
|
+
// This would normally come from a registry service
|
|
108
|
+
// For now we mock the types supported by core
|
|
109
|
+
return { handled: true, response: this.success({ types: ['objects', 'apps', 'plugins'] }) };
|
|
110
|
+
}
|
|
111
|
+
// GET /metadata/:type/:name
|
|
112
|
+
if (parts.length === 2) {
|
|
113
|
+
const [type, name] = parts;
|
|
114
|
+
try {
|
|
115
|
+
// Try specific calls based on type
|
|
116
|
+
if (type === 'objects') {
|
|
117
|
+
const data = await broker.call('metadata.getObject', { objectName: name }, { request: context.request });
|
|
118
|
+
return { handled: true, response: this.success(data) };
|
|
119
|
+
}
|
|
120
|
+
// Generic call for other types if supported
|
|
121
|
+
const data = await broker.call(`metadata.get${this.capitalize(type.slice(0, -1))}`, { name }, { request: context.request });
|
|
122
|
+
return { handled: true, response: this.success(data) };
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
// Fallback: treat first part as object name if only 1 part (handled below)
|
|
126
|
+
// But here we are deep in 2 parts. Must be an error.
|
|
127
|
+
return { handled: true, response: this.error(e.message, 404) };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// GET /metadata/:type (List items of type) OR /metadata/:objectName (Legacy)
|
|
131
|
+
if (parts.length === 1) {
|
|
132
|
+
const typeOrName = parts[0];
|
|
133
|
+
// Heuristic: if it maps to a known type, list it. Else treat as object name.
|
|
134
|
+
if (['objects', 'apps', 'plugins'].includes(typeOrName)) {
|
|
135
|
+
if (typeOrName === 'objects') {
|
|
136
|
+
const data = await broker.call('metadata.objects', {}, { request: context.request });
|
|
137
|
+
return { handled: true, response: this.success(data) };
|
|
138
|
+
}
|
|
139
|
+
// Try generic list
|
|
140
|
+
const data = await broker.call(`metadata.${typeOrName}`, {}, { request: context.request });
|
|
141
|
+
return { handled: true, response: this.success(data) };
|
|
142
|
+
}
|
|
143
|
+
// Legacy: /metadata/:objectName
|
|
144
|
+
const data = await broker.call('metadata.getObject', { objectName: typeOrName }, { request: context.request });
|
|
145
|
+
return { handled: true, response: this.success(data) };
|
|
146
|
+
}
|
|
147
|
+
// GET /metadata (List Objects - Default)
|
|
148
|
+
if (parts.length === 0) {
|
|
149
|
+
const data = await broker.call('metadata.objects', {}, { request: context.request });
|
|
150
|
+
return { handled: true, response: this.success(data) };
|
|
151
|
+
}
|
|
152
|
+
return { handled: false };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Handles Data requests
|
|
156
|
+
* path: sub-path after /data/ (e.g. "contacts", "contacts/123", "contacts/query")
|
|
157
|
+
*/
|
|
158
|
+
async handleData(path, method, body, query, context) {
|
|
159
|
+
const broker = this.ensureBroker();
|
|
160
|
+
const parts = path.replace(/^\/+/, '').split('/');
|
|
161
|
+
const objectName = parts[0];
|
|
162
|
+
if (!objectName) {
|
|
163
|
+
return { handled: true, response: this.error('Object name required', 400) };
|
|
164
|
+
}
|
|
165
|
+
const m = method.toUpperCase();
|
|
166
|
+
// 1. Custom Actions (query, batch)
|
|
167
|
+
if (parts.length > 1) {
|
|
168
|
+
const action = parts[1];
|
|
169
|
+
// POST /data/:object/query
|
|
170
|
+
if (action === 'query' && m === 'POST') {
|
|
171
|
+
const result = await broker.call('data.query', { object: objectName, ...body }, { request: context.request });
|
|
172
|
+
return { handled: true, response: this.success(result.data, { count: result.count, limit: body.limit, skip: body.skip }) };
|
|
173
|
+
}
|
|
174
|
+
// POST /data/:object/batch
|
|
175
|
+
if (action === 'batch' && m === 'POST') {
|
|
176
|
+
// Spec complaint: forward the whole body { operation, records, options }
|
|
177
|
+
// Implementation in Kernel should handle the 'operation' field
|
|
178
|
+
const result = await broker.call('data.batch', { object: objectName, ...body }, { request: context.request });
|
|
179
|
+
return { handled: true, response: this.success(result) };
|
|
180
|
+
}
|
|
181
|
+
// GET /data/:object/:id
|
|
182
|
+
if (parts.length === 2 && m === 'GET') {
|
|
183
|
+
const id = parts[1];
|
|
184
|
+
const data = await broker.call('data.get', { object: objectName, id, ...query }, { request: context.request });
|
|
185
|
+
return { handled: true, response: this.success(data) };
|
|
186
|
+
}
|
|
187
|
+
// PATCH /data/:object/:id
|
|
188
|
+
if (parts.length === 2 && m === 'PATCH') {
|
|
189
|
+
const id = parts[1];
|
|
190
|
+
const data = await broker.call('data.update', { object: objectName, id, data: body }, { request: context.request });
|
|
191
|
+
return { handled: true, response: this.success(data) };
|
|
192
|
+
}
|
|
193
|
+
// DELETE /data/:object/:id
|
|
194
|
+
if (parts.length === 2 && m === 'DELETE') {
|
|
195
|
+
const id = parts[1];
|
|
196
|
+
await broker.call('data.delete', { object: objectName, id }, { request: context.request });
|
|
197
|
+
return { handled: true, response: this.success({ id, deleted: true }) };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// GET /data/:object (List)
|
|
202
|
+
if (m === 'GET') {
|
|
203
|
+
const result = await broker.call('data.query', { object: objectName, filters: query }, { request: context.request });
|
|
204
|
+
return { handled: true, response: this.success(result.data, { count: result.count }) };
|
|
205
|
+
}
|
|
206
|
+
// POST /data/:object (Create)
|
|
207
|
+
if (m === 'POST') {
|
|
208
|
+
const data = await broker.call('data.create', { object: objectName, data: body }, { request: context.request });
|
|
209
|
+
// Note: ideally 201
|
|
210
|
+
const res = this.success(data);
|
|
211
|
+
res.status = 201;
|
|
212
|
+
return { handled: true, response: res };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { handled: false };
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Handles Analytics requests
|
|
219
|
+
* path: sub-path after /analytics/
|
|
220
|
+
*/
|
|
221
|
+
async handleAnalytics(path, method, body, context) {
|
|
222
|
+
const analyticsService = this.getService(CoreServiceName.enum.analytics);
|
|
223
|
+
if (!analyticsService)
|
|
224
|
+
return { handled: false }; // 404 handled by caller if unhandled
|
|
225
|
+
const m = method.toUpperCase();
|
|
226
|
+
const subPath = path.replace(/^\/+/, '');
|
|
227
|
+
// POST /analytics/query
|
|
228
|
+
if (subPath === 'query' && m === 'POST') {
|
|
229
|
+
const result = await analyticsService.query(body, { request: context.request });
|
|
230
|
+
return { handled: true, response: this.success(result) };
|
|
231
|
+
}
|
|
232
|
+
// GET /analytics/meta
|
|
233
|
+
if (subPath === 'meta' && m === 'GET') {
|
|
234
|
+
const result = await analyticsService.getMetadata({ request: context.request });
|
|
235
|
+
return { handled: true, response: this.success(result) };
|
|
236
|
+
}
|
|
237
|
+
// POST /analytics/sql (Dry-run or debug)
|
|
238
|
+
if (subPath === 'sql' && m === 'POST') {
|
|
239
|
+
// Assuming service has generateSql method
|
|
240
|
+
const result = await analyticsService.generateSql(body, { request: context.request });
|
|
241
|
+
return { handled: true, response: this.success(result) };
|
|
242
|
+
}
|
|
243
|
+
return { handled: false };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Handles Hub requests
|
|
247
|
+
* path: sub-path after /hub/
|
|
248
|
+
*/
|
|
249
|
+
async handleHub(path, method, body, query, context) {
|
|
250
|
+
const hubService = this.getService(CoreServiceName.enum.hub);
|
|
251
|
+
if (!hubService)
|
|
252
|
+
return { handled: false };
|
|
253
|
+
const m = method.toUpperCase();
|
|
254
|
+
const parts = path.replace(/^\/+/, '').split('/');
|
|
255
|
+
// Resource-based routing: /hub/:resource/:id
|
|
256
|
+
if (parts.length > 0) {
|
|
257
|
+
const resource = parts[0]; // spaces, plugins, etc.
|
|
258
|
+
// Allow mapping "spaces" -> "createSpace", "listSpaces" etc.
|
|
259
|
+
// Convention:
|
|
260
|
+
// GET /spaces -> listSpaces
|
|
261
|
+
// POST /spaces -> createSpace
|
|
262
|
+
// GET /spaces/:id -> getSpace
|
|
263
|
+
// PATCH /spaces/:id -> updateSpace
|
|
264
|
+
// DELETE /spaces/:id -> deleteSpace
|
|
265
|
+
const actionBase = resource.endsWith('s') ? resource.slice(0, -1) : resource; // space
|
|
266
|
+
const id = parts[1];
|
|
267
|
+
try {
|
|
268
|
+
if (parts.length === 1) {
|
|
269
|
+
// Collection Operations
|
|
270
|
+
if (m === 'GET') {
|
|
271
|
+
const capitalizedAction = 'list' + this.capitalize(resource); // listSpaces
|
|
272
|
+
if (typeof hubService[capitalizedAction] === 'function') {
|
|
273
|
+
const result = await hubService[capitalizedAction](query, { request: context.request });
|
|
274
|
+
return { handled: true, response: this.success(result) };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (m === 'POST') {
|
|
278
|
+
const capitalizedAction = 'create' + this.capitalize(actionBase); // createSpace
|
|
279
|
+
if (typeof hubService[capitalizedAction] === 'function') {
|
|
280
|
+
const result = await hubService[capitalizedAction](body, { request: context.request });
|
|
281
|
+
return { handled: true, response: this.success(result) };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else if (parts.length === 2) {
|
|
286
|
+
// Item Operations
|
|
287
|
+
if (m === 'GET') {
|
|
288
|
+
const capitalizedAction = 'get' + this.capitalize(actionBase); // getSpace
|
|
289
|
+
if (typeof hubService[capitalizedAction] === 'function') {
|
|
290
|
+
const result = await hubService[capitalizedAction](id, { request: context.request });
|
|
291
|
+
return { handled: true, response: this.success(result) };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (m === 'PATCH' || m === 'PUT') {
|
|
295
|
+
const capitalizedAction = 'update' + this.capitalize(actionBase); // updateSpace
|
|
296
|
+
if (typeof hubService[capitalizedAction] === 'function') {
|
|
297
|
+
const result = await hubService[capitalizedAction](id, body, { request: context.request });
|
|
298
|
+
return { handled: true, response: this.success(result) };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (m === 'DELETE') {
|
|
302
|
+
const capitalizedAction = 'delete' + this.capitalize(actionBase); // deleteSpace
|
|
303
|
+
if (typeof hubService[capitalizedAction] === 'function') {
|
|
304
|
+
const result = await hubService[capitalizedAction](id, { request: context.request });
|
|
305
|
+
return { handled: true, response: this.success(result) };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
return { handled: true, response: this.error(e.message, 500) };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { handled: false };
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Handles Storage requests
|
|
318
|
+
* path: sub-path after /storage/
|
|
319
|
+
*/
|
|
320
|
+
async handleStorage(path, method, file, context) {
|
|
321
|
+
const storageService = this.getService(CoreServiceName.enum['file-storage']) || this.kernel.services?.['file-storage'];
|
|
322
|
+
if (!storageService) {
|
|
323
|
+
return { handled: true, response: this.error('File storage not configured', 501) };
|
|
324
|
+
}
|
|
325
|
+
const m = method.toUpperCase();
|
|
326
|
+
const parts = path.replace(/^\/+/, '').split('/');
|
|
327
|
+
// POST /storage/upload
|
|
328
|
+
if (parts[0] === 'upload' && m === 'POST') {
|
|
329
|
+
if (!file) {
|
|
330
|
+
return { handled: true, response: this.error('No file provided', 400) };
|
|
331
|
+
}
|
|
332
|
+
const result = await storageService.upload(file, { request: context.request });
|
|
333
|
+
return { handled: true, response: this.success(result) };
|
|
334
|
+
}
|
|
335
|
+
// GET /storage/file/:id
|
|
336
|
+
if (parts[0] === 'file' && parts[1] && m === 'GET') {
|
|
337
|
+
const id = parts[1];
|
|
338
|
+
const result = await storageService.download(id, { request: context.request });
|
|
339
|
+
// Result can be URL (redirect), Stream/Blob, or metadata
|
|
340
|
+
if (result.url && result.redirect) {
|
|
341
|
+
// Must be handled by adapter to do actual redirect
|
|
342
|
+
return { handled: true, result: { type: 'redirect', url: result.url } };
|
|
343
|
+
}
|
|
344
|
+
if (result.stream) {
|
|
345
|
+
// Must be handled by adapter to pipe stream
|
|
346
|
+
return {
|
|
347
|
+
handled: true,
|
|
348
|
+
result: {
|
|
349
|
+
type: 'stream',
|
|
350
|
+
stream: result.stream,
|
|
351
|
+
headers: {
|
|
352
|
+
'Content-Type': result.mimeType || 'application/octet-stream',
|
|
353
|
+
'Content-Length': result.size
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return { handled: true, response: this.success(result) };
|
|
359
|
+
}
|
|
360
|
+
return { handled: false };
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Handles Automation requests
|
|
364
|
+
* path: sub-path after /automation/
|
|
365
|
+
*/
|
|
366
|
+
async handleAutomation(path, method, body, context) {
|
|
367
|
+
const automationService = this.getService(CoreServiceName.enum.automation);
|
|
368
|
+
if (!automationService)
|
|
369
|
+
return { handled: false };
|
|
370
|
+
const m = method.toUpperCase();
|
|
371
|
+
const parts = path.replace(/^\/+/, '').split('/');
|
|
372
|
+
// POST /automation/trigger/:name
|
|
373
|
+
if (parts[0] === 'trigger' && parts[1] && m === 'POST') {
|
|
374
|
+
const triggerName = parts[1];
|
|
375
|
+
if (typeof automationService.trigger === 'function') {
|
|
376
|
+
const result = await automationService.trigger(triggerName, body, { request: context.request });
|
|
377
|
+
return { handled: true, response: this.success(result) };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return { handled: false };
|
|
381
|
+
}
|
|
382
|
+
getServicesMap() {
|
|
383
|
+
if (this.kernel.services instanceof Map) {
|
|
384
|
+
return Object.fromEntries(this.kernel.services);
|
|
385
|
+
}
|
|
386
|
+
return this.kernel.services || {};
|
|
387
|
+
}
|
|
388
|
+
getService(name) {
|
|
389
|
+
if (typeof this.kernel.getService === 'function') {
|
|
390
|
+
return this.kernel.getService(name);
|
|
391
|
+
}
|
|
392
|
+
const services = this.getServicesMap();
|
|
393
|
+
return services[name];
|
|
394
|
+
}
|
|
395
|
+
capitalize(s) {
|
|
396
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Main Dispatcher Entry Point
|
|
400
|
+
* Routes the request to the appropriate handler based on path and precedence
|
|
401
|
+
*/
|
|
402
|
+
async dispatch(method, path, body, query, context) {
|
|
403
|
+
const cleanPath = path.replace(/\/$/, ''); // Remove trailing slash if present, but strict on clean paths
|
|
404
|
+
// 1. System Protocols (Prefix-based)
|
|
405
|
+
if (cleanPath.startsWith('/auth')) {
|
|
406
|
+
return this.handleAuth(cleanPath.substring(5), method, body, context);
|
|
407
|
+
}
|
|
408
|
+
if (cleanPath.startsWith('/metadata')) {
|
|
409
|
+
return this.handleMetadata(cleanPath.substring(9), context);
|
|
410
|
+
}
|
|
411
|
+
if (cleanPath.startsWith('/data')) {
|
|
412
|
+
return this.handleData(cleanPath.substring(5), method, body, query, context);
|
|
413
|
+
}
|
|
414
|
+
if (cleanPath.startsWith('/graphql')) {
|
|
415
|
+
if (method === 'POST')
|
|
416
|
+
return this.handleGraphQL(body, context);
|
|
417
|
+
// GraphQL usually GET for Playground is handled by middleware but we can return 405 or handle it
|
|
418
|
+
}
|
|
419
|
+
if (cleanPath.startsWith('/storage')) {
|
|
420
|
+
return this.handleStorage(cleanPath.substring(8), method, body, context); // body here is file/stream for upload
|
|
421
|
+
}
|
|
422
|
+
if (cleanPath.startsWith('/automation')) {
|
|
423
|
+
return this.handleAutomation(cleanPath.substring(11), method, body, context);
|
|
424
|
+
}
|
|
425
|
+
if (cleanPath.startsWith('/analytics')) {
|
|
426
|
+
return this.handleAnalytics(cleanPath.substring(10), method, body, context);
|
|
427
|
+
}
|
|
428
|
+
if (cleanPath.startsWith('/hub')) {
|
|
429
|
+
return this.handleHub(cleanPath.substring(4), method, body, query, context);
|
|
430
|
+
}
|
|
431
|
+
// OpenAPI Specification
|
|
432
|
+
if (cleanPath === '/openapi.json' && method === 'GET') {
|
|
433
|
+
const broker = this.ensureBroker();
|
|
434
|
+
try {
|
|
435
|
+
const result = await broker.call('metadata.generateOpenApi', {}, { request: context.request });
|
|
436
|
+
return { handled: true, response: this.success(result) };
|
|
437
|
+
}
|
|
438
|
+
catch (e) {
|
|
439
|
+
// If not implemented, fall through or return 404
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// 2. Custom API Endpoints (Registry lookup)
|
|
443
|
+
// Check if there is a custom endpoint defined for this path
|
|
444
|
+
const result = await this.handleApiEndpoint(cleanPath, method, body, query, context);
|
|
445
|
+
if (result.handled)
|
|
446
|
+
return result;
|
|
447
|
+
// 3. Fallback (404)
|
|
448
|
+
return { handled: false };
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Handles Custom API Endpoints defined in metadata
|
|
452
|
+
*/
|
|
453
|
+
async handleApiEndpoint(path, method, body, query, context) {
|
|
454
|
+
const broker = this.ensureBroker();
|
|
455
|
+
try {
|
|
456
|
+
// Attempt to find a matching endpoint in the registry
|
|
457
|
+
// This assumes a 'metadata.matchEndpoint' action exists in the kernel/registry
|
|
458
|
+
// path should include initial slash e.g. /api/v1/customers
|
|
459
|
+
const endpoint = await broker.call('metadata.matchEndpoint', { path, method });
|
|
460
|
+
if (endpoint) {
|
|
461
|
+
// Execute the endpoint target logic
|
|
462
|
+
if (endpoint.type === 'flow') {
|
|
463
|
+
const result = await broker.call('automation.runFlow', {
|
|
464
|
+
flowId: endpoint.target,
|
|
465
|
+
inputs: { ...query, ...body, _request: context.request }
|
|
466
|
+
});
|
|
467
|
+
return { handled: true, response: this.success(result) };
|
|
468
|
+
}
|
|
469
|
+
if (endpoint.type === 'script') {
|
|
470
|
+
const result = await broker.call('automation.runScript', {
|
|
471
|
+
scriptName: endpoint.target,
|
|
472
|
+
context: { ...query, ...body, request: context.request }
|
|
473
|
+
}, { request: context.request });
|
|
474
|
+
return { handled: true, response: this.success(result) };
|
|
475
|
+
}
|
|
476
|
+
if (endpoint.type === 'object_operation') {
|
|
477
|
+
// e.g. Proxy to an object action
|
|
478
|
+
if (endpoint.objectParams) {
|
|
479
|
+
const { object, operation } = endpoint.objectParams;
|
|
480
|
+
// Map standard CRUD operations
|
|
481
|
+
if (operation === 'find') {
|
|
482
|
+
const result = await broker.call('data.query', { object, filters: query }, { request: context.request });
|
|
483
|
+
return { handled: true, response: this.success(result.data, { count: result.count }) };
|
|
484
|
+
}
|
|
485
|
+
if (operation === 'get' && query.id) {
|
|
486
|
+
const result = await broker.call('data.get', { object, id: query.id }, { request: context.request });
|
|
487
|
+
return { handled: true, response: this.success(result) };
|
|
488
|
+
}
|
|
489
|
+
if (operation === 'create') {
|
|
490
|
+
const result = await broker.call('data.create', { object, data: body }, { request: context.request });
|
|
491
|
+
return { handled: true, response: this.success(result) };
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (endpoint.type === 'proxy') {
|
|
496
|
+
// Simple proxy implementation (requires a network call, which usually is done by a service but here we can stub return)
|
|
497
|
+
// In real implementation this might fetch(endpoint.target)
|
|
498
|
+
// For now, return target info
|
|
499
|
+
return {
|
|
500
|
+
handled: true,
|
|
501
|
+
response: {
|
|
502
|
+
status: 200,
|
|
503
|
+
body: { proxy: true, target: endpoint.target, note: 'Proxy execution requires http-client service' }
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch (e) {
|
|
510
|
+
// If matchEndpoint fails (e.g. not found), we just return not handled
|
|
511
|
+
// so we can fallback to 404 or other handlers
|
|
512
|
+
}
|
|
513
|
+
return { handled: false };
|
|
514
|
+
}
|
|
515
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
export { ObjectKernel } from '@objectstack/core';
|
|
2
|
+
export { Runtime } from './runtime.js';
|
|
3
|
+
export type { RuntimeConfig } from './runtime.js';
|
|
2
4
|
export { DriverPlugin } from './driver-plugin.js';
|
|
3
5
|
export { AppPlugin } from './app-plugin.js';
|
|
6
|
+
export { createApiRegistryPlugin } from './api-registry-plugin.js';
|
|
7
|
+
export type { ApiRegistryConfig } from './api-registry-plugin.js';
|
|
4
8
|
export { HttpServer } from './http-server.js';
|
|
9
|
+
export { HttpDispatcher } from './http-dispatcher.js';
|
|
10
|
+
export type { HttpProtocolContext, HttpDispatcherResult } from './http-dispatcher.js';
|
|
5
11
|
export { RestServer } from './rest-server.js';
|
|
6
12
|
export { RouteManager, RouteGroupBuilder } from './route-manager.js';
|
|
7
13
|
export type { RouteEntry } from './route-manager.js';
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// Export Kernels
|
|
2
2
|
export { ObjectKernel } from '@objectstack/core';
|
|
3
|
+
// Export Runtime
|
|
4
|
+
export { Runtime } from './runtime.js';
|
|
3
5
|
// Export Plugins
|
|
4
6
|
export { DriverPlugin } from './driver-plugin.js';
|
|
5
7
|
export { AppPlugin } from './app-plugin.js';
|
|
8
|
+
export { createApiRegistryPlugin } from './api-registry-plugin.js';
|
|
6
9
|
// Export HTTP Server Components
|
|
7
10
|
export { HttpServer } from './http-server.js';
|
|
11
|
+
export { HttpDispatcher } from './http-dispatcher.js';
|
|
8
12
|
export { RestServer } from './rest-server.js';
|
|
9
13
|
export { RouteManager, RouteGroupBuilder } from './route-manager.js';
|
|
10
14
|
export { MiddlewareManager } from './middleware.js';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ObjectKernel, Plugin, IHttpServer, ObjectKernelConfig } from '@objectstack/core';
|
|
2
|
+
import { ApiRegistryConfig } from './api-registry-plugin.js';
|
|
3
|
+
export interface RuntimeConfig {
|
|
4
|
+
/**
|
|
5
|
+
* Optional existing server instance (e.g. Hono, Express app)
|
|
6
|
+
* If provided, Runtime will use it as the 'http.server' service.
|
|
7
|
+
* If not provided, Runtime expects a server plugin (like HonoServerPlugin) to be registered manually.
|
|
8
|
+
*/
|
|
9
|
+
server?: IHttpServer;
|
|
10
|
+
/**
|
|
11
|
+
* API Registry Configuration
|
|
12
|
+
*/
|
|
13
|
+
api?: ApiRegistryConfig;
|
|
14
|
+
/**
|
|
15
|
+
* Kernel Configuration
|
|
16
|
+
*/
|
|
17
|
+
kernel?: ObjectKernelConfig;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* ObjectStack Runtime
|
|
21
|
+
*
|
|
22
|
+
* High-level entry point for bootstrapping an ObjectStack application.
|
|
23
|
+
* Wraps ObjectKernel and provides standard orchestration for:
|
|
24
|
+
* - HTTP Server binding
|
|
25
|
+
* - API Registry (REST Routes)
|
|
26
|
+
* - Plugin Management
|
|
27
|
+
*/
|
|
28
|
+
export declare class Runtime {
|
|
29
|
+
readonly kernel: ObjectKernel;
|
|
30
|
+
constructor(config?: RuntimeConfig);
|
|
31
|
+
/**
|
|
32
|
+
* Register a plugin
|
|
33
|
+
*/
|
|
34
|
+
use(plugin: Plugin): this;
|
|
35
|
+
/**
|
|
36
|
+
* Start the runtime
|
|
37
|
+
* 1. Initializes all plugins (init phase)
|
|
38
|
+
* 2. Starts all plugins (start phase)
|
|
39
|
+
*/
|
|
40
|
+
start(): Promise<this>;
|
|
41
|
+
/**
|
|
42
|
+
* Get the kernel instance
|
|
43
|
+
*/
|
|
44
|
+
getKernel(): ObjectKernel;
|
|
45
|
+
}
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ObjectKernel } from '@objectstack/core';
|
|
2
|
+
import { createApiRegistryPlugin } from './api-registry-plugin.js';
|
|
3
|
+
/**
|
|
4
|
+
* ObjectStack Runtime
|
|
5
|
+
*
|
|
6
|
+
* High-level entry point for bootstrapping an ObjectStack application.
|
|
7
|
+
* Wraps ObjectKernel and provides standard orchestration for:
|
|
8
|
+
* - HTTP Server binding
|
|
9
|
+
* - API Registry (REST Routes)
|
|
10
|
+
* - Plugin Management
|
|
11
|
+
*/
|
|
12
|
+
export class Runtime {
|
|
13
|
+
constructor(config = {}) {
|
|
14
|
+
this.kernel = new ObjectKernel(config.kernel);
|
|
15
|
+
// If external server provided, register it immediately
|
|
16
|
+
if (config.server) {
|
|
17
|
+
// If the provided server is not already an HttpServer wrapper, wrap it?
|
|
18
|
+
// Since IHttpServer is the interface, we assume it complies.
|
|
19
|
+
// But HttpServer class in runtime is an adapter.
|
|
20
|
+
// If user passes raw Hono, it won't work unless they wrapped it.
|
|
21
|
+
// We'll assume they pass a compliant IHttpServer.
|
|
22
|
+
this.kernel.registerService('http.server', config.server);
|
|
23
|
+
}
|
|
24
|
+
// Register API Registry by default
|
|
25
|
+
// This plugin is passive (wait for services) so it's safe to add early.
|
|
26
|
+
this.kernel.use(createApiRegistryPlugin(config.api));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Register a plugin
|
|
30
|
+
*/
|
|
31
|
+
use(plugin) {
|
|
32
|
+
this.kernel.use(plugin);
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Start the runtime
|
|
37
|
+
* 1. Initializes all plugins (init phase)
|
|
38
|
+
* 2. Starts all plugins (start phase)
|
|
39
|
+
*/
|
|
40
|
+
async start() {
|
|
41
|
+
await this.kernel.bootstrap();
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get the kernel instance
|
|
46
|
+
*/
|
|
47
|
+
getKernel() {
|
|
48
|
+
return this.kernel;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|