@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.
@@ -1,5 +1,5 @@
1
1
  import { Plugin, PluginContext, IHttpServer } from '@objectstack/core';
2
- import { IObjectStackProtocol } from '@objectstack/spec/api';
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.log('[HonoServerPlugin] HTTP server service registered');
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: IObjectStackProtocol | null = null;
52
+ let protocol: ObjectStackProtocol | null = null;
46
53
 
47
54
  try {
48
- protocol = ctx.getService<IObjectStackProtocol>('protocol');
55
+ protocol = ctx.getService<ObjectStackProtocol>('protocol');
56
+ ctx.logger.debug('Protocol service found, registering protocol routes');
49
57
  } catch (e) {
50
- ctx.logger.log('[HonoServerPlugin] Protocol service not found, skipping protocol routes');
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
- this.server.get('/api/v1', (req, res) => res.json(p.getDiscovery()));
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) => res.json(p.getMetaTypes()));
60
- this.server.get('/api/v1/meta/:type', (req, res) => res.json(p.getMetaItems(req.params.type)));
61
- this.server.get('/api/v1/meta/:type/:name', (req, res) => {
62
- try {
63
- res.json(p.getMetaItem(req.params.type, req.params.name));
64
- } catch(e:any) {
65
- res.status(404).json({error: e.message});
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
- try { res.json(await p.findData(req.params.object, req.query)); }
72
- catch(e:any) { res.status(400).json({error:e.message}); }
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
- try { res.json(await p.getData(req.params.object, req.params.id)); }
76
- catch(e:any) { res.status(404).json({error:e.message}); }
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
- try { res.status(201).json(await p.createData(req.params.object, req.body)); }
80
- catch(e:any) { res.status(400).json({error:e.message}); }
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
- try { res.json(await p.updateData(req.params.object, req.params.id, req.body)); }
84
- catch(e:any) { res.status(400).json({error:e.message}); }
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
- try { res.json(await p.deleteData(req.params.object, req.params.id)); }
88
- catch(e:any) { res.status(400).json({error:e.message}); }
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
- // @ts-ignore
93
- this.server.get('/api/v1/ui/view/:object', (req, res) => {
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
- const viewType = (req.query.type) || 'list';
96
- const qt = Array.isArray(viewType) ? viewType[0] : viewType;
97
- res.json(p.getUiView(req.params.object, qt as any));
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.log('[HonoServerPlugin] Starting server...');
375
+ ctx.logger.info('Starting HTTP server', { port });
107
376
 
108
377
  await this.server.listen(port);
109
- ctx.logger.log(`✅ Server is running on http://localhost:${port}`);
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
  }