@objectstack/plugin-hono-server 0.6.0 → 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 +20 -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/src/hono-plugin.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Plugin, PluginContext, IHttpServer } from '@objectstack/core';
|
|
2
|
-
import {
|
|
2
|
+
import { ObjectStackProtocol } from '@objectstack/spec/api';
|
|
3
3
|
import { HonoHttpServer } from './adapter';
|
|
4
4
|
|
|
5
5
|
export interface HonoPluginOptions {
|
|
@@ -32,81 +32,353 @@ export class HonoServerPlugin implements Plugin {
|
|
|
32
32
|
* Init phase - Setup HTTP server and register as service
|
|
33
33
|
*/
|
|
34
34
|
async init(ctx: PluginContext) {
|
|
35
|
+
ctx.logger.debug('Initializing Hono server plugin', {
|
|
36
|
+
port: this.options.port,
|
|
37
|
+
staticRoot: this.options.staticRoot
|
|
38
|
+
});
|
|
39
|
+
|
|
35
40
|
// Register HTTP server service as IHttpServer
|
|
36
41
|
ctx.registerService('http-server', this.server);
|
|
37
|
-
ctx.logger.
|
|
42
|
+
ctx.logger.info('HTTP server service registered', { serviceName: 'http-server' });
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
/**
|
|
41
46
|
* Start phase - Bind routes and start listening
|
|
42
47
|
*/
|
|
43
48
|
async start(ctx: PluginContext) {
|
|
49
|
+
ctx.logger.debug('Starting Hono server plugin');
|
|
50
|
+
|
|
44
51
|
// Get protocol implementation instance
|
|
45
|
-
let protocol:
|
|
52
|
+
let protocol: ObjectStackProtocol | null = null;
|
|
46
53
|
|
|
47
54
|
try {
|
|
48
|
-
protocol = ctx.getService<
|
|
55
|
+
protocol = ctx.getService<ObjectStackProtocol>('protocol');
|
|
56
|
+
ctx.logger.debug('Protocol service found, registering protocol routes');
|
|
49
57
|
} catch (e) {
|
|
50
|
-
ctx.logger.
|
|
58
|
+
ctx.logger.warn('Protocol service not found, skipping protocol routes');
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
// Register protocol routes if available
|
|
54
62
|
if (protocol) {
|
|
55
63
|
const p = protocol!;
|
|
56
|
-
|
|
64
|
+
|
|
65
|
+
ctx.logger.debug('Registering API routes');
|
|
66
|
+
|
|
67
|
+
this.server.get('/api/v1', async (req, res) => {
|
|
68
|
+
ctx.logger.debug('API discovery request');
|
|
69
|
+
res.json(await p.getDiscovery({}));
|
|
70
|
+
});
|
|
57
71
|
|
|
58
72
|
// Meta Protocol
|
|
59
|
-
this.server.get('/api/v1/meta', (req, res) =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
73
|
+
this.server.get('/api/v1/meta', async (req, res) => {
|
|
74
|
+
ctx.logger.debug('Meta types request');
|
|
75
|
+
res.json(await p.getMetaTypes({}));
|
|
76
|
+
});
|
|
77
|
+
this.server.get('/api/v1/meta/:type', async (req, res) => {
|
|
78
|
+
ctx.logger.debug('Meta items request', { type: req.params.type });
|
|
79
|
+
res.json(await p.getMetaItems({ type: req.params.type }));
|
|
67
80
|
});
|
|
68
81
|
|
|
69
82
|
// Data Protocol
|
|
70
83
|
this.server.get('/api/v1/data/:object', async (req, res) => {
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
ctx.logger.debug('Data find request', { object: req.params.object, query: req.query });
|
|
85
|
+
try {
|
|
86
|
+
const result = await p.findData({ object: req.params.object, query: req.query as any });
|
|
87
|
+
ctx.logger.debug('Data find completed', { object: req.params.object, count: result?.records?.length ?? 0 });
|
|
88
|
+
res.json(result);
|
|
89
|
+
}
|
|
90
|
+
catch(e:any) {
|
|
91
|
+
ctx.logger.error('Data find failed', e, { object: req.params.object });
|
|
92
|
+
res.status(400).json({error:e.message});
|
|
93
|
+
}
|
|
73
94
|
});
|
|
74
95
|
this.server.get('/api/v1/data/:object/:id', async (req, res) => {
|
|
75
|
-
|
|
76
|
-
|
|
96
|
+
ctx.logger.debug('Data get request', { object: req.params.object, id: req.params.id });
|
|
97
|
+
try {
|
|
98
|
+
const result = await p.getData({ object: req.params.object, id: req.params.id });
|
|
99
|
+
ctx.logger.debug('Data get completed', { object: req.params.object, id: req.params.id });
|
|
100
|
+
res.json(result);
|
|
101
|
+
}
|
|
102
|
+
catch(e:any) {
|
|
103
|
+
ctx.logger.warn('Data get failed - not found', { object: req.params.object, id: req.params.id });
|
|
104
|
+
res.status(404).json({error:e.message});
|
|
105
|
+
}
|
|
77
106
|
});
|
|
78
107
|
this.server.post('/api/v1/data/:object', async (req, res) => {
|
|
79
|
-
|
|
80
|
-
|
|
108
|
+
ctx.logger.debug('Data create request', { object: req.params.object });
|
|
109
|
+
try {
|
|
110
|
+
const result = await p.createData({ object: req.params.object, data: req.body });
|
|
111
|
+
ctx.logger.info('Data created', { object: req.params.object, id: result?.id });
|
|
112
|
+
res.status(201).json(result);
|
|
113
|
+
}
|
|
114
|
+
catch(e:any) {
|
|
115
|
+
ctx.logger.error('Data create failed', e, { object: req.params.object });
|
|
116
|
+
res.status(400).json({error:e.message});
|
|
117
|
+
}
|
|
81
118
|
});
|
|
82
119
|
this.server.patch('/api/v1/data/:object/:id', async (req, res) => {
|
|
83
|
-
|
|
84
|
-
|
|
120
|
+
ctx.logger.debug('Data update request', { object: req.params.object, id: req.params.id });
|
|
121
|
+
try {
|
|
122
|
+
const result = await p.updateData({ object: req.params.object, id: req.params.id, data: req.body });
|
|
123
|
+
ctx.logger.info('Data updated', { object: req.params.object, id: req.params.id });
|
|
124
|
+
res.json(result);
|
|
125
|
+
}
|
|
126
|
+
catch(e:any) {
|
|
127
|
+
ctx.logger.error('Data update failed', e, { object: req.params.object, id: req.params.id });
|
|
128
|
+
res.status(400).json({error:e.message});
|
|
129
|
+
}
|
|
85
130
|
});
|
|
86
131
|
this.server.delete('/api/v1/data/:object/:id', async (req, res) => {
|
|
87
|
-
|
|
88
|
-
|
|
132
|
+
ctx.logger.debug('Data delete request', { object: req.params.object, id: req.params.id });
|
|
133
|
+
try {
|
|
134
|
+
const result = await p.deleteData({ object: req.params.object, id: req.params.id });
|
|
135
|
+
ctx.logger.info('Data deleted', { object: req.params.object, id: req.params.id, success: result?.success });
|
|
136
|
+
res.json(result);
|
|
137
|
+
}
|
|
138
|
+
catch(e:any) {
|
|
139
|
+
ctx.logger.error('Data delete failed', e, { object: req.params.object, id: req.params.id });
|
|
140
|
+
res.status(400).json({error:e.message});
|
|
141
|
+
}
|
|
89
142
|
});
|
|
90
143
|
|
|
91
144
|
// UI Protocol
|
|
92
|
-
|
|
93
|
-
|
|
145
|
+
this.server.get('/api/v1/ui/view/:object', async (req, res) => {
|
|
146
|
+
const viewType = (req.query.type) || 'list';
|
|
147
|
+
const qt = Array.isArray(viewType) ? viewType[0] : viewType;
|
|
148
|
+
ctx.logger.debug('UI view request', { object: req.params.object, viewType: qt });
|
|
94
149
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
150
|
+
res.json(await p.getUiView({ object: req.params.object, type: qt as any }));
|
|
151
|
+
}
|
|
152
|
+
catch(e:any) {
|
|
153
|
+
ctx.logger.warn('UI view not found', { object: req.params.object, viewType: qt });
|
|
154
|
+
res.status(404).json({error:e.message});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Batch Operations
|
|
159
|
+
this.server.post('/api/v1/data/:object/batch', async (req, res) => {
|
|
160
|
+
ctx.logger.info('Batch operation request', {
|
|
161
|
+
object: req.params.object,
|
|
162
|
+
operation: req.body?.operation,
|
|
163
|
+
hasBody: !!req.body,
|
|
164
|
+
bodyType: typeof req.body,
|
|
165
|
+
bodyKeys: req.body ? Object.keys(req.body) : []
|
|
166
|
+
});
|
|
167
|
+
try {
|
|
168
|
+
const result = await p.batchData({ object: req.params.object, request: req.body });
|
|
169
|
+
ctx.logger.info('Batch operation completed', {
|
|
170
|
+
object: req.params.object,
|
|
171
|
+
operation: req.body?.operation,
|
|
172
|
+
total: result.total,
|
|
173
|
+
succeeded: result.succeeded,
|
|
174
|
+
failed: result.failed
|
|
175
|
+
});
|
|
176
|
+
res.json(result);
|
|
177
|
+
} catch (e: any) {
|
|
178
|
+
ctx.logger.error('Batch operation failed', e, { object: req.params.object });
|
|
179
|
+
res.status(400).json({ error: e.message });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
this.server.post('/api/v1/data/:object/createMany', async (req, res) => {
|
|
184
|
+
ctx.logger.debug('Create many request', { object: req.params.object, count: req.body?.length });
|
|
185
|
+
try {
|
|
186
|
+
const result = await p.createManyData({ object: req.params.object, records: req.body || [] });
|
|
187
|
+
ctx.logger.info('Create many completed', { object: req.params.object, count: result.records?.length ?? 0 });
|
|
188
|
+
res.status(201).json(result);
|
|
189
|
+
} catch (e: any) {
|
|
190
|
+
ctx.logger.error('Create many failed', e, { object: req.params.object });
|
|
191
|
+
res.status(400).json({ error: e.message });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
this.server.post('/api/v1/data/:object/updateMany', async (req, res) => {
|
|
196
|
+
ctx.logger.debug('Update many request', { object: req.params.object, count: req.body?.records?.length });
|
|
197
|
+
try {
|
|
198
|
+
const result = await p.updateManyData({ object: req.params.object, records: req.body?.records, options: req.body?.options });
|
|
199
|
+
ctx.logger.info('Update many completed', {
|
|
200
|
+
object: req.params.object,
|
|
201
|
+
total: result.total,
|
|
202
|
+
succeeded: result.succeeded,
|
|
203
|
+
failed: result.failed
|
|
204
|
+
});
|
|
205
|
+
res.json(result);
|
|
206
|
+
} catch (e: any) {
|
|
207
|
+
ctx.logger.error('Update many failed', e, { object: req.params.object });
|
|
208
|
+
res.status(400).json({ error: e.message });
|
|
98
209
|
}
|
|
99
|
-
catch(e:any) { res.status(404).json({error:e.message}); }
|
|
100
210
|
});
|
|
211
|
+
|
|
212
|
+
this.server.post('/api/v1/data/:object/deleteMany', async (req, res) => {
|
|
213
|
+
ctx.logger.debug('Delete many request', { object: req.params.object, count: req.body?.ids?.length });
|
|
214
|
+
try {
|
|
215
|
+
const result = await p.deleteManyData({ object: req.params.object, ids: req.body?.ids, options: req.body?.options });
|
|
216
|
+
ctx.logger.info('Delete many completed', {
|
|
217
|
+
object: req.params.object,
|
|
218
|
+
total: result.total,
|
|
219
|
+
succeeded: result.succeeded,
|
|
220
|
+
failed: result.failed
|
|
221
|
+
});
|
|
222
|
+
res.json(result);
|
|
223
|
+
} catch (e: any) {
|
|
224
|
+
ctx.logger.error('Delete many failed', e, { object: req.params.object });
|
|
225
|
+
res.status(400).json({ error: e.message });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Enhanced Metadata Route with ETag Support
|
|
230
|
+
this.server.get('/api/v1/meta/:type/:name', async (req, res) => {
|
|
231
|
+
ctx.logger.debug('Meta item request with cache support', {
|
|
232
|
+
type: req.params.type,
|
|
233
|
+
name: req.params.name,
|
|
234
|
+
ifNoneMatch: req.headers['if-none-match']
|
|
235
|
+
});
|
|
236
|
+
try {
|
|
237
|
+
const cacheRequest = {
|
|
238
|
+
ifNoneMatch: req.headers['if-none-match'] as string,
|
|
239
|
+
ifModifiedSince: req.headers['if-modified-since'] as string,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const result = await p.getMetaItemCached({
|
|
243
|
+
type: req.params.type,
|
|
244
|
+
name: req.params.name,
|
|
245
|
+
cacheRequest
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
if (result.notModified) {
|
|
249
|
+
ctx.logger.debug('Meta item not modified (304)', { type: req.params.type, name: req.params.name });
|
|
250
|
+
res.status(304).json({});
|
|
251
|
+
} else {
|
|
252
|
+
// Set cache headers
|
|
253
|
+
if (result.etag) {
|
|
254
|
+
const etagValue = result.etag.weak ? `W/"${result.etag.value}"` : `"${result.etag.value}"`;
|
|
255
|
+
res.header('ETag', etagValue);
|
|
256
|
+
}
|
|
257
|
+
if (result.lastModified) {
|
|
258
|
+
res.header('Last-Modified', new Date(result.lastModified).toUTCString());
|
|
259
|
+
}
|
|
260
|
+
if (result.cacheControl) {
|
|
261
|
+
const directives = result.cacheControl.directives.join(', ');
|
|
262
|
+
const maxAge = result.cacheControl.maxAge ? `, max-age=${result.cacheControl.maxAge}` : '';
|
|
263
|
+
res.header('Cache-Control', directives + maxAge);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
ctx.logger.debug('Meta item returned with cache headers', {
|
|
267
|
+
type: req.params.type,
|
|
268
|
+
name: req.params.name,
|
|
269
|
+
etag: result.etag?.value
|
|
270
|
+
});
|
|
271
|
+
res.json(result.data);
|
|
272
|
+
}
|
|
273
|
+
} catch (e: any) {
|
|
274
|
+
ctx.logger.warn('Meta item not found', { type: req.params.type, name: req.params.name });
|
|
275
|
+
res.status(404).json({ error: e.message });
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// View Storage Routes
|
|
280
|
+
this.server.post('/api/v1/ui/views', async (req, res) => {
|
|
281
|
+
ctx.logger.debug('Create view request', { name: req.body?.name, object: req.body?.object });
|
|
282
|
+
try {
|
|
283
|
+
const result = await p.createView(req.body);
|
|
284
|
+
if (result.success) {
|
|
285
|
+
ctx.logger.info('View created', { id: result.data?.id, name: result.data?.name });
|
|
286
|
+
res.status(201).json(result);
|
|
287
|
+
} else {
|
|
288
|
+
ctx.logger.warn('View creation failed', { error: result.error });
|
|
289
|
+
res.status(400).json(result);
|
|
290
|
+
}
|
|
291
|
+
} catch (e: any) {
|
|
292
|
+
ctx.logger.error('View creation error', e);
|
|
293
|
+
res.status(500).json({ success: false, error: { code: 'internal_error', message: e.message } });
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
this.server.get('/api/v1/ui/views/:id', async (req, res) => {
|
|
298
|
+
ctx.logger.debug('Get view request', { id: req.params.id });
|
|
299
|
+
try {
|
|
300
|
+
const result = await p.getView({ id: req.params.id });
|
|
301
|
+
if (result.success) {
|
|
302
|
+
ctx.logger.debug('View retrieved', { id: req.params.id });
|
|
303
|
+
res.json(result);
|
|
304
|
+
} else {
|
|
305
|
+
ctx.logger.warn('View not found', { id: req.params.id });
|
|
306
|
+
res.status(404).json(result);
|
|
307
|
+
}
|
|
308
|
+
} catch (e: any) {
|
|
309
|
+
ctx.logger.error('Get view error', e, { id: req.params.id });
|
|
310
|
+
res.status(500).json({ success: false, error: { code: 'internal_error', message: e.message } });
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
this.server.get('/api/v1/ui/views', async (req, res) => {
|
|
315
|
+
ctx.logger.debug('List views request', { query: req.query });
|
|
316
|
+
try {
|
|
317
|
+
const request: any = {};
|
|
318
|
+
if (req.query.object) request.object = req.query.object as string;
|
|
319
|
+
if (req.query.type) request.type = req.query.type;
|
|
320
|
+
if (req.query.visibility) request.visibility = req.query.visibility;
|
|
321
|
+
if (req.query.createdBy) request.createdBy = req.query.createdBy as string;
|
|
322
|
+
if (req.query.isDefault !== undefined) request.isDefault = req.query.isDefault === 'true';
|
|
323
|
+
if (req.query.limit) request.limit = parseInt(req.query.limit as string);
|
|
324
|
+
if (req.query.offset) request.offset = parseInt(req.query.offset as string);
|
|
325
|
+
|
|
326
|
+
const result = await p.listViews(request);
|
|
327
|
+
ctx.logger.debug('Views listed', { count: result.data?.length, total: result.pagination?.total });
|
|
328
|
+
res.json(result);
|
|
329
|
+
} catch (e: any) {
|
|
330
|
+
ctx.logger.error('List views error', e);
|
|
331
|
+
res.status(500).json({ success: false, error: { code: 'internal_error', message: e.message } });
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
this.server.patch('/api/v1/ui/views/:id', async (req, res) => {
|
|
336
|
+
ctx.logger.debug('Update view request', { id: req.params.id });
|
|
337
|
+
try {
|
|
338
|
+
const result = await p.updateView({ ...req.body, id: req.params.id });
|
|
339
|
+
if (result.success) {
|
|
340
|
+
ctx.logger.info('View updated', { id: req.params.id });
|
|
341
|
+
res.json(result);
|
|
342
|
+
} else {
|
|
343
|
+
ctx.logger.warn('View update failed', { id: req.params.id, error: result.error });
|
|
344
|
+
res.status(result.error?.code === 'resource_not_found' ? 404 : 400).json(result);
|
|
345
|
+
}
|
|
346
|
+
} catch (e: any) {
|
|
347
|
+
ctx.logger.error('Update view error', e, { id: req.params.id });
|
|
348
|
+
res.status(500).json({ success: false, error: { code: 'internal_error', message: e.message } });
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
this.server.delete('/api/v1/ui/views/:id', async (req, res) => {
|
|
353
|
+
ctx.logger.debug('Delete view request', { id: req.params.id });
|
|
354
|
+
try {
|
|
355
|
+
const result = await p.deleteView({ id: req.params.id });
|
|
356
|
+
if (result.success) {
|
|
357
|
+
ctx.logger.info('View deleted', { id: req.params.id });
|
|
358
|
+
res.json(result);
|
|
359
|
+
} else {
|
|
360
|
+
ctx.logger.warn('View deletion failed', { id: req.params.id });
|
|
361
|
+
res.status(404).json(result);
|
|
362
|
+
}
|
|
363
|
+
} catch (e: any) {
|
|
364
|
+
ctx.logger.error('Delete view error', e, { id: req.params.id });
|
|
365
|
+
res.status(500).json({ success: false, error: { code: 'internal_error', message: e.message } });
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
ctx.logger.info('All API routes registered');
|
|
101
370
|
}
|
|
102
371
|
|
|
103
372
|
// Start server on kernel:ready hook
|
|
104
373
|
ctx.hook('kernel:ready', async () => {
|
|
105
374
|
const port = this.options.port || 3000;
|
|
106
|
-
ctx.logger.
|
|
375
|
+
ctx.logger.info('Starting HTTP server', { port });
|
|
107
376
|
|
|
108
377
|
await this.server.listen(port);
|
|
109
|
-
ctx.logger.
|
|
378
|
+
ctx.logger.info('HTTP server started successfully', {
|
|
379
|
+
port,
|
|
380
|
+
url: `http://localhost:${port}`
|
|
381
|
+
});
|
|
110
382
|
});
|
|
111
383
|
}
|
|
112
384
|
|
|
@@ -115,6 +387,7 @@ export class HonoServerPlugin implements Plugin {
|
|
|
115
387
|
*/
|
|
116
388
|
async destroy() {
|
|
117
389
|
this.server.close();
|
|
390
|
+
// Note: Can't use ctx.logger here since we're in destroy
|
|
118
391
|
console.log('[HonoServerPlugin] Server stopped');
|
|
119
392
|
}
|
|
120
393
|
}
|