@objectstack/plugin-hono-server 0.6.1 → 0.7.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/CHANGELOG.md +10 -0
- package/README.md +324 -0
- package/dist/adapter.js +22 -8
- package/dist/hono-plugin.js +277 -24
- package/objectstack.config.ts +238 -0
- package/package.json +4 -4
- package/src/adapter.ts +20 -6
- package/src/hono-plugin.ts +305 -32
package/dist/hono-plugin.js
CHANGED
|
@@ -24,97 +24,349 @@ class HonoServerPlugin {
|
|
|
24
24
|
* Init phase - Setup HTTP server and register as service
|
|
25
25
|
*/
|
|
26
26
|
async init(ctx) {
|
|
27
|
+
ctx.logger.debug('Initializing Hono server plugin', {
|
|
28
|
+
port: this.options.port,
|
|
29
|
+
staticRoot: this.options.staticRoot
|
|
30
|
+
});
|
|
27
31
|
// Register HTTP server service as IHttpServer
|
|
28
32
|
ctx.registerService('http-server', this.server);
|
|
29
|
-
ctx.logger.
|
|
33
|
+
ctx.logger.info('HTTP server service registered', { serviceName: 'http-server' });
|
|
30
34
|
}
|
|
31
35
|
/**
|
|
32
36
|
* Start phase - Bind routes and start listening
|
|
33
37
|
*/
|
|
34
38
|
async start(ctx) {
|
|
39
|
+
ctx.logger.debug('Starting Hono server plugin');
|
|
35
40
|
// Get protocol implementation instance
|
|
36
41
|
let protocol = null;
|
|
37
42
|
try {
|
|
38
43
|
protocol = ctx.getService('protocol');
|
|
44
|
+
ctx.logger.debug('Protocol service found, registering protocol routes');
|
|
39
45
|
}
|
|
40
46
|
catch (e) {
|
|
41
|
-
ctx.logger.
|
|
47
|
+
ctx.logger.warn('Protocol service not found, skipping protocol routes');
|
|
42
48
|
}
|
|
43
49
|
// Register protocol routes if available
|
|
44
50
|
if (protocol) {
|
|
45
51
|
const p = protocol;
|
|
46
|
-
|
|
52
|
+
ctx.logger.debug('Registering API routes');
|
|
53
|
+
this.server.get('/api/v1', async (req, res) => {
|
|
54
|
+
ctx.logger.debug('API discovery request');
|
|
55
|
+
res.json(await p.getDiscovery({}));
|
|
56
|
+
});
|
|
47
57
|
// Meta Protocol
|
|
48
|
-
this.server.get('/api/v1/meta', (req, res) =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
res.status(404).json({ error: e.message });
|
|
56
|
-
}
|
|
58
|
+
this.server.get('/api/v1/meta', async (req, res) => {
|
|
59
|
+
ctx.logger.debug('Meta types request');
|
|
60
|
+
res.json(await p.getMetaTypes({}));
|
|
61
|
+
});
|
|
62
|
+
this.server.get('/api/v1/meta/:type', async (req, res) => {
|
|
63
|
+
ctx.logger.debug('Meta items request', { type: req.params.type });
|
|
64
|
+
res.json(await p.getMetaItems({ type: req.params.type }));
|
|
57
65
|
});
|
|
58
66
|
// Data Protocol
|
|
59
67
|
this.server.get('/api/v1/data/:object', async (req, res) => {
|
|
68
|
+
ctx.logger.debug('Data find request', { object: req.params.object, query: req.query });
|
|
60
69
|
try {
|
|
61
|
-
|
|
70
|
+
const result = await p.findData({ object: req.params.object, query: req.query });
|
|
71
|
+
ctx.logger.debug('Data find completed', { object: req.params.object, count: result?.records?.length ?? 0 });
|
|
72
|
+
res.json(result);
|
|
62
73
|
}
|
|
63
74
|
catch (e) {
|
|
75
|
+
ctx.logger.error('Data find failed', e, { object: req.params.object });
|
|
64
76
|
res.status(400).json({ error: e.message });
|
|
65
77
|
}
|
|
66
78
|
});
|
|
67
79
|
this.server.get('/api/v1/data/:object/:id', async (req, res) => {
|
|
80
|
+
ctx.logger.debug('Data get request', { object: req.params.object, id: req.params.id });
|
|
68
81
|
try {
|
|
69
|
-
|
|
82
|
+
const result = await p.getData({ object: req.params.object, id: req.params.id });
|
|
83
|
+
ctx.logger.debug('Data get completed', { object: req.params.object, id: req.params.id });
|
|
84
|
+
res.json(result);
|
|
70
85
|
}
|
|
71
86
|
catch (e) {
|
|
87
|
+
ctx.logger.warn('Data get failed - not found', { object: req.params.object, id: req.params.id });
|
|
72
88
|
res.status(404).json({ error: e.message });
|
|
73
89
|
}
|
|
74
90
|
});
|
|
75
91
|
this.server.post('/api/v1/data/:object', async (req, res) => {
|
|
92
|
+
ctx.logger.debug('Data create request', { object: req.params.object });
|
|
76
93
|
try {
|
|
77
|
-
|
|
94
|
+
const result = await p.createData({ object: req.params.object, data: req.body });
|
|
95
|
+
ctx.logger.info('Data created', { object: req.params.object, id: result?.id });
|
|
96
|
+
res.status(201).json(result);
|
|
78
97
|
}
|
|
79
98
|
catch (e) {
|
|
99
|
+
ctx.logger.error('Data create failed', e, { object: req.params.object });
|
|
80
100
|
res.status(400).json({ error: e.message });
|
|
81
101
|
}
|
|
82
102
|
});
|
|
83
103
|
this.server.patch('/api/v1/data/:object/:id', async (req, res) => {
|
|
104
|
+
ctx.logger.debug('Data update request', { object: req.params.object, id: req.params.id });
|
|
84
105
|
try {
|
|
85
|
-
|
|
106
|
+
const result = await p.updateData({ object: req.params.object, id: req.params.id, data: req.body });
|
|
107
|
+
ctx.logger.info('Data updated', { object: req.params.object, id: req.params.id });
|
|
108
|
+
res.json(result);
|
|
86
109
|
}
|
|
87
110
|
catch (e) {
|
|
111
|
+
ctx.logger.error('Data update failed', e, { object: req.params.object, id: req.params.id });
|
|
88
112
|
res.status(400).json({ error: e.message });
|
|
89
113
|
}
|
|
90
114
|
});
|
|
91
115
|
this.server.delete('/api/v1/data/:object/:id', async (req, res) => {
|
|
116
|
+
ctx.logger.debug('Data delete request', { object: req.params.object, id: req.params.id });
|
|
92
117
|
try {
|
|
93
|
-
|
|
118
|
+
const result = await p.deleteData({ object: req.params.object, id: req.params.id });
|
|
119
|
+
ctx.logger.info('Data deleted', { object: req.params.object, id: req.params.id, success: result?.success });
|
|
120
|
+
res.json(result);
|
|
94
121
|
}
|
|
95
122
|
catch (e) {
|
|
123
|
+
ctx.logger.error('Data delete failed', e, { object: req.params.object, id: req.params.id });
|
|
96
124
|
res.status(400).json({ error: e.message });
|
|
97
125
|
}
|
|
98
126
|
});
|
|
99
127
|
// UI Protocol
|
|
100
|
-
|
|
101
|
-
|
|
128
|
+
this.server.get('/api/v1/ui/view/:object', async (req, res) => {
|
|
129
|
+
const viewType = (req.query.type) || 'list';
|
|
130
|
+
const qt = Array.isArray(viewType) ? viewType[0] : viewType;
|
|
131
|
+
ctx.logger.debug('UI view request', { object: req.params.object, viewType: qt });
|
|
132
|
+
try {
|
|
133
|
+
res.json(await p.getUiView({ object: req.params.object, type: qt }));
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
ctx.logger.warn('UI view not found', { object: req.params.object, viewType: qt });
|
|
137
|
+
res.status(404).json({ error: e.message });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// Batch Operations
|
|
141
|
+
this.server.post('/api/v1/data/:object/batch', async (req, res) => {
|
|
142
|
+
ctx.logger.info('Batch operation request', {
|
|
143
|
+
object: req.params.object,
|
|
144
|
+
operation: req.body?.operation,
|
|
145
|
+
hasBody: !!req.body,
|
|
146
|
+
bodyType: typeof req.body,
|
|
147
|
+
bodyKeys: req.body ? Object.keys(req.body) : []
|
|
148
|
+
});
|
|
149
|
+
try {
|
|
150
|
+
const result = await p.batchData({ object: req.params.object, request: req.body });
|
|
151
|
+
ctx.logger.info('Batch operation completed', {
|
|
152
|
+
object: req.params.object,
|
|
153
|
+
operation: req.body?.operation,
|
|
154
|
+
total: result.total,
|
|
155
|
+
succeeded: result.succeeded,
|
|
156
|
+
failed: result.failed
|
|
157
|
+
});
|
|
158
|
+
res.json(result);
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
ctx.logger.error('Batch operation failed', e, { object: req.params.object });
|
|
162
|
+
res.status(400).json({ error: e.message });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
this.server.post('/api/v1/data/:object/createMany', async (req, res) => {
|
|
166
|
+
ctx.logger.debug('Create many request', { object: req.params.object, count: req.body?.length });
|
|
167
|
+
try {
|
|
168
|
+
const result = await p.createManyData({ object: req.params.object, records: req.body || [] });
|
|
169
|
+
ctx.logger.info('Create many completed', { object: req.params.object, count: result.records?.length ?? 0 });
|
|
170
|
+
res.status(201).json(result);
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
ctx.logger.error('Create many failed', e, { object: req.params.object });
|
|
174
|
+
res.status(400).json({ error: e.message });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
this.server.post('/api/v1/data/:object/updateMany', async (req, res) => {
|
|
178
|
+
ctx.logger.debug('Update many request', { object: req.params.object, count: req.body?.records?.length });
|
|
179
|
+
try {
|
|
180
|
+
const result = await p.updateManyData({ object: req.params.object, records: req.body?.records, options: req.body?.options });
|
|
181
|
+
ctx.logger.info('Update many completed', {
|
|
182
|
+
object: req.params.object,
|
|
183
|
+
total: result.total,
|
|
184
|
+
succeeded: result.succeeded,
|
|
185
|
+
failed: result.failed
|
|
186
|
+
});
|
|
187
|
+
res.json(result);
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
ctx.logger.error('Update many failed', e, { object: req.params.object });
|
|
191
|
+
res.status(400).json({ error: e.message });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
this.server.post('/api/v1/data/:object/deleteMany', async (req, res) => {
|
|
195
|
+
ctx.logger.debug('Delete many request', { object: req.params.object, count: req.body?.ids?.length });
|
|
102
196
|
try {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
197
|
+
const result = await p.deleteManyData({ object: req.params.object, ids: req.body?.ids, options: req.body?.options });
|
|
198
|
+
ctx.logger.info('Delete many completed', {
|
|
199
|
+
object: req.params.object,
|
|
200
|
+
total: result.total,
|
|
201
|
+
succeeded: result.succeeded,
|
|
202
|
+
failed: result.failed
|
|
203
|
+
});
|
|
204
|
+
res.json(result);
|
|
106
205
|
}
|
|
107
206
|
catch (e) {
|
|
207
|
+
ctx.logger.error('Delete many failed', e, { object: req.params.object });
|
|
208
|
+
res.status(400).json({ error: e.message });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
// Enhanced Metadata Route with ETag Support
|
|
212
|
+
this.server.get('/api/v1/meta/:type/:name', async (req, res) => {
|
|
213
|
+
ctx.logger.debug('Meta item request with cache support', {
|
|
214
|
+
type: req.params.type,
|
|
215
|
+
name: req.params.name,
|
|
216
|
+
ifNoneMatch: req.headers['if-none-match']
|
|
217
|
+
});
|
|
218
|
+
try {
|
|
219
|
+
const cacheRequest = {
|
|
220
|
+
ifNoneMatch: req.headers['if-none-match'],
|
|
221
|
+
ifModifiedSince: req.headers['if-modified-since'],
|
|
222
|
+
};
|
|
223
|
+
const result = await p.getMetaItemCached({
|
|
224
|
+
type: req.params.type,
|
|
225
|
+
name: req.params.name,
|
|
226
|
+
cacheRequest
|
|
227
|
+
});
|
|
228
|
+
if (result.notModified) {
|
|
229
|
+
ctx.logger.debug('Meta item not modified (304)', { type: req.params.type, name: req.params.name });
|
|
230
|
+
res.status(304).json({});
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
// Set cache headers
|
|
234
|
+
if (result.etag) {
|
|
235
|
+
const etagValue = result.etag.weak ? `W/"${result.etag.value}"` : `"${result.etag.value}"`;
|
|
236
|
+
res.header('ETag', etagValue);
|
|
237
|
+
}
|
|
238
|
+
if (result.lastModified) {
|
|
239
|
+
res.header('Last-Modified', new Date(result.lastModified).toUTCString());
|
|
240
|
+
}
|
|
241
|
+
if (result.cacheControl) {
|
|
242
|
+
const directives = result.cacheControl.directives.join(', ');
|
|
243
|
+
const maxAge = result.cacheControl.maxAge ? `, max-age=${result.cacheControl.maxAge}` : '';
|
|
244
|
+
res.header('Cache-Control', directives + maxAge);
|
|
245
|
+
}
|
|
246
|
+
ctx.logger.debug('Meta item returned with cache headers', {
|
|
247
|
+
type: req.params.type,
|
|
248
|
+
name: req.params.name,
|
|
249
|
+
etag: result.etag?.value
|
|
250
|
+
});
|
|
251
|
+
res.json(result.data);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (e) {
|
|
255
|
+
ctx.logger.warn('Meta item not found', { type: req.params.type, name: req.params.name });
|
|
108
256
|
res.status(404).json({ error: e.message });
|
|
109
257
|
}
|
|
110
258
|
});
|
|
259
|
+
// View Storage Routes
|
|
260
|
+
this.server.post('/api/v1/ui/views', async (req, res) => {
|
|
261
|
+
ctx.logger.debug('Create view request', { name: req.body?.name, object: req.body?.object });
|
|
262
|
+
try {
|
|
263
|
+
const result = await p.createView(req.body);
|
|
264
|
+
if (result.success) {
|
|
265
|
+
ctx.logger.info('View created', { id: result.data?.id, name: result.data?.name });
|
|
266
|
+
res.status(201).json(result);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
ctx.logger.warn('View creation failed', { error: result.error });
|
|
270
|
+
res.status(400).json(result);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (e) {
|
|
274
|
+
ctx.logger.error('View creation error', e);
|
|
275
|
+
res.status(500).json({ success: false, error: { code: 'internal_error', message: e.message } });
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
this.server.get('/api/v1/ui/views/:id', async (req, res) => {
|
|
279
|
+
ctx.logger.debug('Get view request', { id: req.params.id });
|
|
280
|
+
try {
|
|
281
|
+
const result = await p.getView({ id: req.params.id });
|
|
282
|
+
if (result.success) {
|
|
283
|
+
ctx.logger.debug('View retrieved', { id: req.params.id });
|
|
284
|
+
res.json(result);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
ctx.logger.warn('View not found', { id: req.params.id });
|
|
288
|
+
res.status(404).json(result);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
ctx.logger.error('Get view error', e, { id: req.params.id });
|
|
293
|
+
res.status(500).json({ success: false, error: { code: 'internal_error', message: e.message } });
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
this.server.get('/api/v1/ui/views', async (req, res) => {
|
|
297
|
+
ctx.logger.debug('List views request', { query: req.query });
|
|
298
|
+
try {
|
|
299
|
+
const request = {};
|
|
300
|
+
if (req.query.object)
|
|
301
|
+
request.object = req.query.object;
|
|
302
|
+
if (req.query.type)
|
|
303
|
+
request.type = req.query.type;
|
|
304
|
+
if (req.query.visibility)
|
|
305
|
+
request.visibility = req.query.visibility;
|
|
306
|
+
if (req.query.createdBy)
|
|
307
|
+
request.createdBy = req.query.createdBy;
|
|
308
|
+
if (req.query.isDefault !== undefined)
|
|
309
|
+
request.isDefault = req.query.isDefault === 'true';
|
|
310
|
+
if (req.query.limit)
|
|
311
|
+
request.limit = parseInt(req.query.limit);
|
|
312
|
+
if (req.query.offset)
|
|
313
|
+
request.offset = parseInt(req.query.offset);
|
|
314
|
+
const result = await p.listViews(request);
|
|
315
|
+
ctx.logger.debug('Views listed', { count: result.data?.length, total: result.pagination?.total });
|
|
316
|
+
res.json(result);
|
|
317
|
+
}
|
|
318
|
+
catch (e) {
|
|
319
|
+
ctx.logger.error('List views error', e);
|
|
320
|
+
res.status(500).json({ success: false, error: { code: 'internal_error', message: e.message } });
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
this.server.patch('/api/v1/ui/views/:id', async (req, res) => {
|
|
324
|
+
ctx.logger.debug('Update view request', { id: req.params.id });
|
|
325
|
+
try {
|
|
326
|
+
const result = await p.updateView({ ...req.body, id: req.params.id });
|
|
327
|
+
if (result.success) {
|
|
328
|
+
ctx.logger.info('View updated', { id: req.params.id });
|
|
329
|
+
res.json(result);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
ctx.logger.warn('View update failed', { id: req.params.id, error: result.error });
|
|
333
|
+
res.status(result.error?.code === 'resource_not_found' ? 404 : 400).json(result);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
catch (e) {
|
|
337
|
+
ctx.logger.error('Update view error', e, { id: req.params.id });
|
|
338
|
+
res.status(500).json({ success: false, error: { code: 'internal_error', message: e.message } });
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
this.server.delete('/api/v1/ui/views/:id', async (req, res) => {
|
|
342
|
+
ctx.logger.debug('Delete view request', { id: req.params.id });
|
|
343
|
+
try {
|
|
344
|
+
const result = await p.deleteView({ id: req.params.id });
|
|
345
|
+
if (result.success) {
|
|
346
|
+
ctx.logger.info('View deleted', { id: req.params.id });
|
|
347
|
+
res.json(result);
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
ctx.logger.warn('View deletion failed', { id: req.params.id });
|
|
351
|
+
res.status(404).json(result);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch (e) {
|
|
355
|
+
ctx.logger.error('Delete view error', e, { id: req.params.id });
|
|
356
|
+
res.status(500).json({ success: false, error: { code: 'internal_error', message: e.message } });
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
ctx.logger.info('All API routes registered');
|
|
111
360
|
}
|
|
112
361
|
// Start server on kernel:ready hook
|
|
113
362
|
ctx.hook('kernel:ready', async () => {
|
|
114
363
|
const port = this.options.port || 3000;
|
|
115
|
-
ctx.logger.
|
|
364
|
+
ctx.logger.info('Starting HTTP server', { port });
|
|
116
365
|
await this.server.listen(port);
|
|
117
|
-
ctx.logger.
|
|
366
|
+
ctx.logger.info('HTTP server started successfully', {
|
|
367
|
+
port,
|
|
368
|
+
url: `http://localhost:${port}`
|
|
369
|
+
});
|
|
118
370
|
});
|
|
119
371
|
}
|
|
120
372
|
/**
|
|
@@ -122,6 +374,7 @@ class HonoServerPlugin {
|
|
|
122
374
|
*/
|
|
123
375
|
async destroy() {
|
|
124
376
|
this.server.close();
|
|
377
|
+
// Note: Can't use ctx.logger here since we're in destroy
|
|
125
378
|
console.log('[HonoServerPlugin] Server stopped');
|
|
126
379
|
}
|
|
127
380
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { ObjectStackManifest } from '@objectstack/spec/system';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hono Server Plugin Manifest
|
|
5
|
+
*
|
|
6
|
+
* HTTP server adapter plugin using the Hono framework.
|
|
7
|
+
* Provides northbound HTTP/REST API gateway capabilities.
|
|
8
|
+
*/
|
|
9
|
+
const HonoServerPlugin: ObjectStackManifest = {
|
|
10
|
+
id: 'com.objectstack.server.hono',
|
|
11
|
+
name: 'Hono Server Adapter',
|
|
12
|
+
version: '1.0.0',
|
|
13
|
+
type: 'adapter',
|
|
14
|
+
description: 'HTTP server adapter using Hono framework. Exposes ObjectStack Runtime Protocol via REST API endpoints.',
|
|
15
|
+
|
|
16
|
+
configuration: {
|
|
17
|
+
title: 'Hono Server Configuration',
|
|
18
|
+
properties: {
|
|
19
|
+
port: {
|
|
20
|
+
type: 'number',
|
|
21
|
+
default: 3000,
|
|
22
|
+
description: 'HTTP server port',
|
|
23
|
+
},
|
|
24
|
+
staticRoot: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Path to static files directory (optional)',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
// Plugin Capability Declaration
|
|
32
|
+
capabilities: {
|
|
33
|
+
// Protocols This Plugin Implements
|
|
34
|
+
implements: [
|
|
35
|
+
{
|
|
36
|
+
protocol: {
|
|
37
|
+
id: 'com.objectstack.protocol.http.v1',
|
|
38
|
+
label: 'HTTP Server Protocol v1',
|
|
39
|
+
version: { major: 1, minor: 0, patch: 0 },
|
|
40
|
+
description: 'Standard HTTP server capabilities',
|
|
41
|
+
},
|
|
42
|
+
conformance: 'full',
|
|
43
|
+
certified: false,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
protocol: {
|
|
47
|
+
id: 'com.objectstack.protocol.api.rest.v1',
|
|
48
|
+
label: 'REST API Protocol v1',
|
|
49
|
+
version: { major: 1, minor: 0, patch: 0 },
|
|
50
|
+
description: 'RESTful API endpoint implementation',
|
|
51
|
+
},
|
|
52
|
+
conformance: 'full',
|
|
53
|
+
features: [
|
|
54
|
+
{
|
|
55
|
+
name: 'meta_protocol',
|
|
56
|
+
enabled: true,
|
|
57
|
+
description: 'Metadata discovery endpoints',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'data_protocol',
|
|
61
|
+
enabled: true,
|
|
62
|
+
description: 'CRUD data operations',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'ui_protocol',
|
|
66
|
+
enabled: true,
|
|
67
|
+
description: 'UI view metadata endpoints',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
certified: false,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
|
|
74
|
+
// Interfaces This Plugin Provides
|
|
75
|
+
provides: [
|
|
76
|
+
{
|
|
77
|
+
id: 'com.objectstack.server.hono.interface.http_server',
|
|
78
|
+
name: 'IHttpServer',
|
|
79
|
+
description: 'HTTP server service interface',
|
|
80
|
+
version: { major: 1, minor: 0, patch: 0 },
|
|
81
|
+
stability: 'stable',
|
|
82
|
+
methods: [
|
|
83
|
+
{
|
|
84
|
+
name: 'get',
|
|
85
|
+
description: 'Register GET route handler',
|
|
86
|
+
parameters: [
|
|
87
|
+
{
|
|
88
|
+
name: 'path',
|
|
89
|
+
type: 'string',
|
|
90
|
+
required: true,
|
|
91
|
+
description: 'Route path pattern',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'handler',
|
|
95
|
+
type: 'Function',
|
|
96
|
+
required: true,
|
|
97
|
+
description: 'Route handler function',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
returnType: 'void',
|
|
101
|
+
async: false,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'post',
|
|
105
|
+
description: 'Register POST route handler',
|
|
106
|
+
parameters: [
|
|
107
|
+
{
|
|
108
|
+
name: 'path',
|
|
109
|
+
type: 'string',
|
|
110
|
+
required: true,
|
|
111
|
+
description: 'Route path pattern',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'handler',
|
|
115
|
+
type: 'Function',
|
|
116
|
+
required: true,
|
|
117
|
+
description: 'Route handler function',
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
returnType: 'void',
|
|
121
|
+
async: false,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'patch',
|
|
125
|
+
description: 'Register PATCH route handler',
|
|
126
|
+
parameters: [
|
|
127
|
+
{
|
|
128
|
+
name: 'path',
|
|
129
|
+
type: 'string',
|
|
130
|
+
required: true,
|
|
131
|
+
description: 'Route path pattern',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'handler',
|
|
135
|
+
type: 'Function',
|
|
136
|
+
required: true,
|
|
137
|
+
description: 'Route handler function',
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
returnType: 'void',
|
|
141
|
+
async: false,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'delete',
|
|
145
|
+
description: 'Register DELETE route handler',
|
|
146
|
+
parameters: [
|
|
147
|
+
{
|
|
148
|
+
name: 'path',
|
|
149
|
+
type: 'string',
|
|
150
|
+
required: true,
|
|
151
|
+
description: 'Route path pattern',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'handler',
|
|
155
|
+
type: 'Function',
|
|
156
|
+
required: true,
|
|
157
|
+
description: 'Route handler function',
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
returnType: 'void',
|
|
161
|
+
async: false,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'listen',
|
|
165
|
+
description: 'Start the HTTP server',
|
|
166
|
+
parameters: [
|
|
167
|
+
{
|
|
168
|
+
name: 'port',
|
|
169
|
+
type: 'number',
|
|
170
|
+
required: true,
|
|
171
|
+
description: 'Port number',
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
returnType: 'Promise<void>',
|
|
175
|
+
async: true,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'close',
|
|
179
|
+
description: 'Stop the HTTP server',
|
|
180
|
+
parameters: [],
|
|
181
|
+
returnType: 'void',
|
|
182
|
+
async: false,
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
|
|
188
|
+
// Dependencies on Other Plugins/Services
|
|
189
|
+
requires: [
|
|
190
|
+
{
|
|
191
|
+
pluginId: 'com.objectstack.engine.objectql',
|
|
192
|
+
version: '^0.6.0',
|
|
193
|
+
optional: true,
|
|
194
|
+
reason: 'ObjectStack Runtime Protocol implementation service',
|
|
195
|
+
requiredCapabilities: [
|
|
196
|
+
'com.objectstack.protocol.runtime.v1',
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
|
|
201
|
+
// Extension Points This Plugin Defines
|
|
202
|
+
extensionPoints: [
|
|
203
|
+
{
|
|
204
|
+
id: 'com.objectstack.server.hono.extension.middleware',
|
|
205
|
+
name: 'HTTP Middleware',
|
|
206
|
+
description: 'Register custom HTTP middleware',
|
|
207
|
+
type: 'hook',
|
|
208
|
+
cardinality: 'multiple',
|
|
209
|
+
contract: {
|
|
210
|
+
signature: '(req: Request, res: Response, next: Function) => void | Promise<void>',
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: 'com.objectstack.server.hono.extension.route',
|
|
215
|
+
name: 'Custom Routes',
|
|
216
|
+
description: 'Register custom API routes',
|
|
217
|
+
type: 'action',
|
|
218
|
+
cardinality: 'multiple',
|
|
219
|
+
contract: {
|
|
220
|
+
input: 'RouteDefinition',
|
|
221
|
+
signature: '(app: HonoApp) => void',
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
|
|
226
|
+
// No extensions contributed to other plugins
|
|
227
|
+
extensions: [],
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
contributes: {
|
|
231
|
+
// System Events
|
|
232
|
+
events: [
|
|
233
|
+
'kernel:ready',
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export default HonoServerPlugin;
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/plugin-hono-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Standard Hono Server Adapter for ObjectStack Runtime",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@hono/node-server": "^1.2.0",
|
|
9
9
|
"hono": "^4.0.0",
|
|
10
|
-
"@objectstack/core": "0.
|
|
11
|
-
"@objectstack/spec": "0.
|
|
12
|
-
"@objectstack/types": "0.
|
|
10
|
+
"@objectstack/core": "0.7.1",
|
|
11
|
+
"@objectstack/spec": "0.7.1",
|
|
12
|
+
"@objectstack/types": "0.7.1"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@types/node": "^20.0.0",
|
package/src/adapter.ts
CHANGED
|
@@ -27,19 +27,33 @@ export class HonoHttpServer implements IHttpServer {
|
|
|
27
27
|
// internal helper to convert standard handler to Hono handler
|
|
28
28
|
private wrap(handler: RouteHandler) {
|
|
29
29
|
return async (c: any) => {
|
|
30
|
+
let body: any = {};
|
|
31
|
+
|
|
32
|
+
// Try to parse JSON body first if content-type is JSON
|
|
33
|
+
if (c.req.header('content-type')?.includes('application/json')) {
|
|
34
|
+
try {
|
|
35
|
+
body = await c.req.json();
|
|
36
|
+
} catch(e) {
|
|
37
|
+
// If JSON parsing fails, try parseBody
|
|
38
|
+
try {
|
|
39
|
+
body = await c.req.parseBody();
|
|
40
|
+
} catch(e2) {}
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
// For non-JSON content types, use parseBody
|
|
44
|
+
try {
|
|
45
|
+
body = await c.req.parseBody();
|
|
46
|
+
} catch(e) {}
|
|
47
|
+
}
|
|
48
|
+
|
|
30
49
|
const req = {
|
|
31
50
|
params: c.req.param(),
|
|
32
51
|
query: c.req.query(),
|
|
33
|
-
body
|
|
52
|
+
body,
|
|
34
53
|
headers: c.req.header(),
|
|
35
54
|
method: c.req.method,
|
|
36
55
|
path: c.req.path
|
|
37
56
|
};
|
|
38
|
-
|
|
39
|
-
// Try to parse JSON body if possible
|
|
40
|
-
if (c.req.header('content-type')?.includes('application/json')) {
|
|
41
|
-
try { req.body = await c.req.json(); } catch(e) {}
|
|
42
|
-
}
|
|
43
57
|
|
|
44
58
|
let capturedResponse: any;
|
|
45
59
|
|