@objectstack/plugin-hono-server 0.8.2 → 0.9.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.
@@ -10,12 +10,18 @@ const adapter_1 = require("./adapter");
10
10
  */
11
11
  class HonoServerPlugin {
12
12
  name = 'com.objectstack.server.hono';
13
- version = '1.0.0';
13
+ version = '0.9.0';
14
+ // Constants
15
+ static DEFAULT_ENDPOINT_PRIORITY = 100;
16
+ static CORE_ENDPOINT_PRIORITY = 950;
17
+ static DISCOVERY_ENDPOINT_PRIORITY = 900;
14
18
  options;
15
19
  server;
16
20
  constructor(options = {}) {
17
21
  this.options = {
18
22
  port: 3000,
23
+ registerStandardEndpoints: true,
24
+ useApiRegistry: true,
19
25
  ...options
20
26
  };
21
27
  this.server = new adapter_1.HonoHttpServer(this.options.port, this.options.staticRoot);
@@ -32,6 +38,15 @@ class HonoServerPlugin {
32
38
  ctx.registerService('http-server', this.server);
33
39
  ctx.logger.info('HTTP server service registered', { serviceName: 'http-server' });
34
40
  }
41
+ /**
42
+ * Helper to create cache request object from HTTP headers
43
+ */
44
+ createCacheRequest(headers) {
45
+ return {
46
+ ifNoneMatch: headers['if-none-match'],
47
+ ifModifiedSince: headers['if-modified-since'],
48
+ };
49
+ }
35
50
  /**
36
51
  * Start phase - Bind routes and start listening
37
52
  */
@@ -41,31 +56,544 @@ class HonoServerPlugin {
41
56
  let protocol = null;
42
57
  try {
43
58
  protocol = ctx.getService('protocol');
44
- ctx.logger.debug('Protocol service found, registering protocol routes');
59
+ ctx.logger.debug('Protocol service found');
45
60
  }
46
61
  catch (e) {
47
62
  ctx.logger.warn('Protocol service not found, skipping protocol routes');
48
63
  }
49
- // Register protocol routes if available
64
+ // Try to get API Registry
65
+ let apiRegistry = null;
66
+ try {
67
+ apiRegistry = ctx.getService('api-registry');
68
+ ctx.logger.debug('API Registry found, will use for endpoint registration');
69
+ }
70
+ catch (e) {
71
+ ctx.logger.debug('API Registry not found, using legacy route registration');
72
+ }
73
+ // Register standard ObjectStack endpoints
50
74
  if (protocol) {
51
- const p = protocol;
52
- ctx.logger.debug('Registering API routes');
53
- this.server.get('/api/v1', async (req, res) => {
75
+ if (apiRegistry && this.options.registerStandardEndpoints) {
76
+ this.registerStandardEndpointsToRegistry(apiRegistry, ctx);
77
+ }
78
+ // Bind routes from registry or fallback to legacy
79
+ if (apiRegistry && this.options.useApiRegistry) {
80
+ this.bindRoutesFromRegistry(apiRegistry, protocol, ctx);
81
+ }
82
+ else {
83
+ this.bindLegacyRoutes(protocol, ctx);
84
+ }
85
+ }
86
+ // Start server on kernel:ready hook
87
+ ctx.hook('kernel:ready', async () => {
88
+ const port = this.options.port || 3000;
89
+ ctx.logger.info('Starting HTTP server', { port });
90
+ await this.server.listen(port);
91
+ ctx.logger.info('HTTP server started successfully', {
92
+ port,
93
+ url: `http://localhost:${port}`
94
+ });
95
+ });
96
+ }
97
+ /**
98
+ * Register standard ObjectStack API endpoints to the API Registry
99
+ */
100
+ registerStandardEndpointsToRegistry(registry, ctx) {
101
+ const config = this.options.restConfig || {};
102
+ const apiVersion = config.api?.version || 'v1';
103
+ const basePath = config.api?.basePath || '/api';
104
+ const apiPath = config.api?.apiPath || `${basePath}/${apiVersion}`;
105
+ const endpoints = [];
106
+ // Discovery endpoint
107
+ if (config.api?.enableDiscovery !== false) {
108
+ endpoints.push({
109
+ id: 'get_discovery',
110
+ method: 'GET',
111
+ path: apiPath,
112
+ summary: 'API Discovery',
113
+ description: 'Get API version and capabilities',
114
+ responses: [{
115
+ statusCode: 200,
116
+ description: 'API discovery information'
117
+ }],
118
+ priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
119
+ });
120
+ }
121
+ // Metadata endpoints
122
+ if (config.api?.enableMetadata !== false) {
123
+ const metaPrefix = config.metadata?.prefix || '/meta';
124
+ endpoints.push({
125
+ id: 'get_meta_types',
126
+ method: 'GET',
127
+ path: `${apiPath}${metaPrefix}`,
128
+ summary: 'Get Metadata Types',
129
+ description: 'List all available metadata types',
130
+ responses: [{
131
+ statusCode: 200,
132
+ description: 'List of metadata types'
133
+ }],
134
+ priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
135
+ }, {
136
+ id: 'get_meta_items',
137
+ method: 'GET',
138
+ path: `${apiPath}${metaPrefix}/:type`,
139
+ summary: 'Get Metadata Items',
140
+ description: 'Get all items of a metadata type',
141
+ parameters: [{
142
+ name: 'type',
143
+ in: 'path',
144
+ required: true,
145
+ schema: { type: 'string' }
146
+ }],
147
+ responses: [{
148
+ statusCode: 200,
149
+ description: 'List of metadata items'
150
+ }],
151
+ priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
152
+ }, {
153
+ id: 'get_meta_item_cached',
154
+ method: 'GET',
155
+ path: `${apiPath}${metaPrefix}/:type/:name`,
156
+ summary: 'Get Metadata Item with Cache',
157
+ description: 'Get a specific metadata item with ETag support',
158
+ parameters: [
159
+ {
160
+ name: 'type',
161
+ in: 'path',
162
+ required: true,
163
+ schema: { type: 'string' }
164
+ },
165
+ {
166
+ name: 'name',
167
+ in: 'path',
168
+ required: true,
169
+ schema: { type: 'string' }
170
+ }
171
+ ],
172
+ responses: [
173
+ {
174
+ statusCode: 200,
175
+ description: 'Metadata item',
176
+ headers: {
177
+ 'ETag': { description: 'Entity tag for caching', schema: { type: 'string' } },
178
+ 'Last-Modified': { description: 'Last modification time', schema: { type: 'string' } },
179
+ 'Cache-Control': { description: 'Cache directives', schema: { type: 'string' } }
180
+ }
181
+ },
182
+ {
183
+ statusCode: 304,
184
+ description: 'Not Modified'
185
+ }
186
+ ],
187
+ priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
188
+ });
189
+ }
190
+ // CRUD endpoints
191
+ if (config.api?.enableCrud !== false) {
192
+ const dataPrefix = config.crud?.dataPrefix || '/data';
193
+ endpoints.push(
194
+ // List/Query
195
+ {
196
+ id: 'find_data',
197
+ method: 'GET',
198
+ path: `${apiPath}${dataPrefix}/:object`,
199
+ summary: 'Find Records',
200
+ description: 'Query records from an object',
201
+ parameters: [{
202
+ name: 'object',
203
+ in: 'path',
204
+ required: true,
205
+ schema: { type: 'string' }
206
+ }],
207
+ responses: [{
208
+ statusCode: 200,
209
+ description: 'List of records'
210
+ }],
211
+ priority: HonoServerPlugin.CORE_ENDPOINT_PRIORITY
212
+ },
213
+ // Get by ID
214
+ {
215
+ id: 'get_data',
216
+ method: 'GET',
217
+ path: `${apiPath}${dataPrefix}/:object/:id`,
218
+ summary: 'Get Record by ID',
219
+ description: 'Retrieve a single record by its ID',
220
+ parameters: [
221
+ {
222
+ name: 'object',
223
+ in: 'path',
224
+ required: true,
225
+ schema: { type: 'string' }
226
+ },
227
+ {
228
+ name: 'id',
229
+ in: 'path',
230
+ required: true,
231
+ schema: { type: 'string' }
232
+ }
233
+ ],
234
+ responses: [
235
+ {
236
+ statusCode: 200,
237
+ description: 'Record found'
238
+ },
239
+ {
240
+ statusCode: 404,
241
+ description: 'Record not found'
242
+ }
243
+ ],
244
+ priority: HonoServerPlugin.CORE_ENDPOINT_PRIORITY
245
+ },
246
+ // Create
247
+ {
248
+ id: 'create_data',
249
+ method: 'POST',
250
+ path: `${apiPath}${dataPrefix}/:object`,
251
+ summary: 'Create Record',
252
+ description: 'Create a new record',
253
+ parameters: [{
254
+ name: 'object',
255
+ in: 'path',
256
+ required: true,
257
+ schema: { type: 'string' }
258
+ }],
259
+ requestBody: {
260
+ required: true,
261
+ description: 'Record data'
262
+ },
263
+ responses: [{
264
+ statusCode: 201,
265
+ description: 'Record created'
266
+ }],
267
+ priority: HonoServerPlugin.CORE_ENDPOINT_PRIORITY
268
+ },
269
+ // Update
270
+ {
271
+ id: 'update_data',
272
+ method: 'PATCH',
273
+ path: `${apiPath}${dataPrefix}/:object/:id`,
274
+ summary: 'Update Record',
275
+ description: 'Update an existing record',
276
+ parameters: [
277
+ {
278
+ name: 'object',
279
+ in: 'path',
280
+ required: true,
281
+ schema: { type: 'string' }
282
+ },
283
+ {
284
+ name: 'id',
285
+ in: 'path',
286
+ required: true,
287
+ schema: { type: 'string' }
288
+ }
289
+ ],
290
+ requestBody: {
291
+ required: true,
292
+ description: 'Fields to update'
293
+ },
294
+ responses: [{
295
+ statusCode: 200,
296
+ description: 'Record updated'
297
+ }],
298
+ priority: HonoServerPlugin.CORE_ENDPOINT_PRIORITY
299
+ },
300
+ // Delete
301
+ {
302
+ id: 'delete_data',
303
+ method: 'DELETE',
304
+ path: `${apiPath}${dataPrefix}/:object/:id`,
305
+ summary: 'Delete Record',
306
+ description: 'Delete a record by ID',
307
+ parameters: [
308
+ {
309
+ name: 'object',
310
+ in: 'path',
311
+ required: true,
312
+ schema: { type: 'string' }
313
+ },
314
+ {
315
+ name: 'id',
316
+ in: 'path',
317
+ required: true,
318
+ schema: { type: 'string' }
319
+ }
320
+ ],
321
+ responses: [{
322
+ statusCode: 200,
323
+ description: 'Record deleted'
324
+ }],
325
+ priority: HonoServerPlugin.CORE_ENDPOINT_PRIORITY
326
+ });
327
+ }
328
+ // Batch endpoints
329
+ if (config.api?.enableBatch !== false) {
330
+ const dataPrefix = config.crud?.dataPrefix || '/data';
331
+ endpoints.push({
332
+ id: 'batch_data',
333
+ method: 'POST',
334
+ path: `${apiPath}${dataPrefix}/:object/batch`,
335
+ summary: 'Batch Operations',
336
+ description: 'Perform batch create/update/delete operations',
337
+ parameters: [{
338
+ name: 'object',
339
+ in: 'path',
340
+ required: true,
341
+ schema: { type: 'string' }
342
+ }],
343
+ requestBody: {
344
+ required: true,
345
+ description: 'Batch operation request'
346
+ },
347
+ responses: [{
348
+ statusCode: 200,
349
+ description: 'Batch operation completed'
350
+ }],
351
+ priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
352
+ }, {
353
+ id: 'create_many_data',
354
+ method: 'POST',
355
+ path: `${apiPath}${dataPrefix}/:object/createMany`,
356
+ summary: 'Create Multiple Records',
357
+ description: 'Create multiple records in one request',
358
+ parameters: [{
359
+ name: 'object',
360
+ in: 'path',
361
+ required: true,
362
+ schema: { type: 'string' }
363
+ }],
364
+ requestBody: {
365
+ required: true,
366
+ description: 'Array of records to create'
367
+ },
368
+ responses: [{
369
+ statusCode: 201,
370
+ description: 'Records created'
371
+ }],
372
+ priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
373
+ }, {
374
+ id: 'update_many_data',
375
+ method: 'POST',
376
+ path: `${apiPath}${dataPrefix}/:object/updateMany`,
377
+ summary: 'Update Multiple Records',
378
+ description: 'Update multiple records in one request',
379
+ parameters: [{
380
+ name: 'object',
381
+ in: 'path',
382
+ required: true,
383
+ schema: { type: 'string' }
384
+ }],
385
+ requestBody: {
386
+ required: true,
387
+ description: 'Array of records to update'
388
+ },
389
+ responses: [{
390
+ statusCode: 200,
391
+ description: 'Records updated'
392
+ }],
393
+ priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
394
+ }, {
395
+ id: 'delete_many_data',
396
+ method: 'POST',
397
+ path: `${apiPath}${dataPrefix}/:object/deleteMany`,
398
+ summary: 'Delete Multiple Records',
399
+ description: 'Delete multiple records in one request',
400
+ parameters: [{
401
+ name: 'object',
402
+ in: 'path',
403
+ required: true,
404
+ schema: { type: 'string' }
405
+ }],
406
+ requestBody: {
407
+ required: true,
408
+ description: 'Array of record IDs to delete'
409
+ },
410
+ responses: [{
411
+ statusCode: 200,
412
+ description: 'Records deleted'
413
+ }],
414
+ priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
415
+ });
416
+ }
417
+ // UI endpoints
418
+ endpoints.push({
419
+ id: 'get_ui_view',
420
+ method: 'GET',
421
+ path: `${apiPath}/ui/view/:object`,
422
+ summary: 'Get UI View',
423
+ description: 'Get UI view definition for an object',
424
+ parameters: [
425
+ {
426
+ name: 'object',
427
+ in: 'path',
428
+ required: true,
429
+ schema: { type: 'string' }
430
+ },
431
+ {
432
+ name: 'type',
433
+ in: 'query',
434
+ schema: {
435
+ type: 'string',
436
+ enum: ['list', 'form'],
437
+ default: 'list'
438
+ }
439
+ }
440
+ ],
441
+ responses: [
442
+ {
443
+ statusCode: 200,
444
+ description: 'UI view definition'
445
+ },
446
+ {
447
+ statusCode: 404,
448
+ description: 'View not found'
449
+ }
450
+ ],
451
+ priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
452
+ });
453
+ // Register the API in the registry
454
+ const apiEntry = {
455
+ id: 'objectstack_core_api',
456
+ name: 'ObjectStack Core API',
457
+ type: 'rest',
458
+ version: apiVersion,
459
+ basePath: apiPath,
460
+ description: 'Standard ObjectStack CRUD and metadata API',
461
+ endpoints,
462
+ metadata: {
463
+ owner: 'objectstack',
464
+ status: 'active',
465
+ tags: ['core', 'crud', 'metadata']
466
+ }
467
+ };
468
+ try {
469
+ registry.registerApi(apiEntry);
470
+ ctx.logger.info('Standard ObjectStack endpoints registered to API Registry', {
471
+ endpointCount: endpoints.length
472
+ });
473
+ }
474
+ catch (error) {
475
+ ctx.logger.error('Failed to register standard endpoints', error);
476
+ }
477
+ }
478
+ /**
479
+ * Bind HTTP routes from API Registry
480
+ */
481
+ bindRoutesFromRegistry(registry, protocol, ctx) {
482
+ const apiRegistry = registry.getRegistry();
483
+ ctx.logger.debug('Binding routes from API Registry', {
484
+ totalApis: apiRegistry.totalApis,
485
+ totalEndpoints: apiRegistry.totalEndpoints
486
+ });
487
+ // Get all endpoints sorted by priority (highest first)
488
+ const allEndpoints = [];
489
+ for (const api of apiRegistry.apis) {
490
+ for (const endpoint of api.endpoints) {
491
+ allEndpoints.push({ api: api.id, endpoint });
492
+ }
493
+ }
494
+ // Sort by priority (highest first)
495
+ allEndpoints.sort((a, b) => (b.endpoint.priority || HonoServerPlugin.DEFAULT_ENDPOINT_PRIORITY) -
496
+ (a.endpoint.priority || HonoServerPlugin.DEFAULT_ENDPOINT_PRIORITY));
497
+ // Bind routes
498
+ for (const { endpoint } of allEndpoints) {
499
+ this.bindEndpoint(endpoint, protocol, ctx);
500
+ }
501
+ ctx.logger.info('Routes bound from API Registry', {
502
+ totalRoutes: allEndpoints.length
503
+ });
504
+ }
505
+ /**
506
+ * Bind a single endpoint to the HTTP server
507
+ */
508
+ bindEndpoint(endpoint, protocol, ctx) {
509
+ const method = endpoint.method || 'GET';
510
+ const path = endpoint.path;
511
+ const id = endpoint.id;
512
+ // Map endpoint ID to protocol method
513
+ const handler = this.createHandlerForEndpoint(id, protocol, ctx);
514
+ if (!handler) {
515
+ ctx.logger.warn('No handler found for endpoint', { id, method, path });
516
+ return;
517
+ }
518
+ // Register route based on method
519
+ switch (method.toUpperCase()) {
520
+ case 'GET':
521
+ this.server.get(path, handler);
522
+ break;
523
+ case 'POST':
524
+ this.server.post(path, handler);
525
+ break;
526
+ case 'PATCH':
527
+ this.server.patch(path, handler);
528
+ break;
529
+ case 'PUT':
530
+ this.server.put(path, handler);
531
+ break;
532
+ case 'DELETE':
533
+ this.server.delete(path, handler);
534
+ break;
535
+ default:
536
+ ctx.logger.warn('Unsupported HTTP method', { method, path });
537
+ }
538
+ ctx.logger.debug('Route bound', { method, path, endpoint: id });
539
+ }
540
+ /**
541
+ * Create a route handler for an endpoint
542
+ */
543
+ createHandlerForEndpoint(endpointId, protocol, ctx) {
544
+ const p = protocol;
545
+ // Map endpoint IDs to protocol methods
546
+ const handlerMap = {
547
+ 'get_discovery': async (req, res) => {
54
548
  ctx.logger.debug('API discovery request');
55
549
  res.json(await p.getDiscovery({}));
56
- });
57
- // Meta Protocol
58
- this.server.get('/api/v1/meta', async (req, res) => {
550
+ },
551
+ 'get_meta_types': async (req, res) => {
59
552
  ctx.logger.debug('Meta types request');
60
553
  res.json(await p.getMetaTypes({}));
61
- });
62
- this.server.get('/api/v1/meta/:type', async (req, res) => {
554
+ },
555
+ 'get_meta_items': async (req, res) => {
63
556
  ctx.logger.debug('Meta items request', { type: req.params.type });
64
557
  res.json(await p.getMetaItems({ type: req.params.type }));
65
- });
66
- // Data Protocol
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 });
558
+ },
559
+ 'get_meta_item_cached': async (req, res) => {
560
+ ctx.logger.debug('Meta item cached request', {
561
+ type: req.params.type,
562
+ name: req.params.name
563
+ });
564
+ try {
565
+ const result = await p.getMetaItemCached({
566
+ type: req.params.type,
567
+ name: req.params.name,
568
+ cacheRequest: this.createCacheRequest(req.headers)
569
+ });
570
+ if (result.notModified) {
571
+ res.status(304).send('');
572
+ }
573
+ else {
574
+ // Set cache headers
575
+ if (result.etag) {
576
+ const etagValue = result.etag.weak ? `W/"${result.etag.value}"` : `"${result.etag.value}"`;
577
+ res.header('ETag', etagValue);
578
+ }
579
+ if (result.lastModified) {
580
+ res.header('Last-Modified', new Date(result.lastModified).toUTCString());
581
+ }
582
+ if (result.cacheControl) {
583
+ const directives = result.cacheControl.directives.join(', ');
584
+ const maxAge = result.cacheControl.maxAge ? `, max-age=${result.cacheControl.maxAge}` : '';
585
+ res.header('Cache-Control', directives + maxAge);
586
+ }
587
+ res.json(result.data);
588
+ }
589
+ }
590
+ catch (e) {
591
+ ctx.logger.warn('Meta item not found', { type: req.params.type, name: req.params.name });
592
+ res.status(404).json({ error: e.message });
593
+ }
594
+ },
595
+ 'find_data': async (req, res) => {
596
+ ctx.logger.debug('Data find request', { object: req.params.object });
69
597
  try {
70
598
  const result = await p.findData({ object: req.params.object, query: req.query });
71
599
  ctx.logger.debug('Data find completed', { object: req.params.object, count: result?.records?.length ?? 0 });
@@ -75,20 +603,19 @@ class HonoServerPlugin {
75
603
  ctx.logger.error('Data find failed', e, { object: req.params.object });
76
604
  res.status(400).json({ error: e.message });
77
605
  }
78
- });
79
- this.server.get('/api/v1/data/:object/:id', async (req, res) => {
606
+ },
607
+ 'get_data': async (req, res) => {
80
608
  ctx.logger.debug('Data get request', { object: req.params.object, id: req.params.id });
81
609
  try {
82
610
  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
611
  res.json(result);
85
612
  }
86
613
  catch (e) {
87
- ctx.logger.warn('Data get failed - not found', { object: req.params.object, id: req.params.id });
614
+ ctx.logger.warn('Data get failed', { object: req.params.object, id: req.params.id });
88
615
  res.status(404).json({ error: e.message });
89
616
  }
90
- });
91
- this.server.post('/api/v1/data/:object', async (req, res) => {
617
+ },
618
+ 'create_data': async (req, res) => {
92
619
  ctx.logger.debug('Data create request', { object: req.params.object });
93
620
  try {
94
621
  const result = await p.createData({ object: req.params.object, data: req.body });
@@ -99,8 +626,8 @@ class HonoServerPlugin {
99
626
  ctx.logger.error('Data create failed', e, { object: req.params.object });
100
627
  res.status(400).json({ error: e.message });
101
628
  }
102
- });
103
- this.server.patch('/api/v1/data/:object/:id', async (req, res) => {
629
+ },
630
+ 'update_data': async (req, res) => {
104
631
  ctx.logger.debug('Data update request', { object: req.params.object, id: req.params.id });
105
632
  try {
106
633
  const result = await p.updateData({ object: req.params.object, id: req.params.id, data: req.body });
@@ -111,46 +638,25 @@ class HonoServerPlugin {
111
638
  ctx.logger.error('Data update failed', e, { object: req.params.object, id: req.params.id });
112
639
  res.status(400).json({ error: e.message });
113
640
  }
114
- });
115
- this.server.delete('/api/v1/data/:object/:id', async (req, res) => {
641
+ },
642
+ 'delete_data': async (req, res) => {
116
643
  ctx.logger.debug('Data delete request', { object: req.params.object, id: req.params.id });
117
644
  try {
118
645
  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 });
646
+ ctx.logger.info('Data deleted', { object: req.params.object, id: req.params.id });
120
647
  res.json(result);
121
648
  }
122
649
  catch (e) {
123
650
  ctx.logger.error('Data delete failed', e, { object: req.params.object, id: req.params.id });
124
651
  res.status(400).json({ error: e.message });
125
652
  }
126
- });
127
- // UI Protocol
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
- });
653
+ },
654
+ 'batch_data': async (req, res) => {
655
+ ctx.logger.info('Batch operation request', { object: req.params.object });
149
656
  try {
150
657
  const result = await p.batchData({ object: req.params.object, request: req.body });
151
658
  ctx.logger.info('Batch operation completed', {
152
659
  object: req.params.object,
153
- operation: req.body?.operation,
154
660
  total: result.total,
155
661
  succeeded: result.succeeded,
156
662
  failed: result.failed
@@ -161,9 +667,9 @@ class HonoServerPlugin {
161
667
  ctx.logger.error('Batch operation failed', e, { object: req.params.object });
162
668
  res.status(400).json({ error: e.message });
163
669
  }
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 });
670
+ },
671
+ 'create_many_data': async (req, res) => {
672
+ ctx.logger.debug('Create many request', { object: req.params.object });
167
673
  try {
168
674
  const result = await p.createManyData({ object: req.params.object, records: req.body || [] });
169
675
  ctx.logger.info('Create many completed', { object: req.params.object, count: result.records?.length ?? 0 });
@@ -173,11 +679,15 @@ class HonoServerPlugin {
173
679
  ctx.logger.error('Create many failed', e, { object: req.params.object });
174
680
  res.status(400).json({ error: e.message });
175
681
  }
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 });
682
+ },
683
+ 'update_many_data': async (req, res) => {
684
+ ctx.logger.debug('Update many request', { object: req.params.object });
179
685
  try {
180
- const result = await p.updateManyData({ object: req.params.object, records: req.body?.records, options: req.body?.options });
686
+ const result = await p.updateManyData({
687
+ object: req.params.object,
688
+ records: req.body?.records,
689
+ options: req.body?.options
690
+ });
181
691
  ctx.logger.info('Update many completed', {
182
692
  object: req.params.object,
183
693
  total: result.total,
@@ -190,11 +700,15 @@ class HonoServerPlugin {
190
700
  ctx.logger.error('Update many failed', e, { object: req.params.object });
191
701
  res.status(400).json({ error: e.message });
192
702
  }
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 });
703
+ },
704
+ 'delete_many_data': async (req, res) => {
705
+ ctx.logger.debug('Delete many request', { object: req.params.object });
196
706
  try {
197
- const result = await p.deleteManyData({ object: req.params.object, ids: req.body?.ids, options: req.body?.options });
707
+ const result = await p.deleteManyData({
708
+ object: req.params.object,
709
+ ids: req.body?.ids,
710
+ options: req.body?.options
711
+ });
198
712
  ctx.logger.info('Delete many completed', {
199
713
  object: req.params.object,
200
714
  total: result.total,
@@ -207,80 +721,233 @@ class HonoServerPlugin {
207
721
  ctx.logger.error('Delete many failed', e, { object: req.params.object });
208
722
  res.status(400).json({ error: e.message });
209
723
  }
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 });
256
- res.status(404).json({ error: e.message });
257
- }
258
- });
259
- // UI Protocol endpoint
260
- this.server.get('/api/v1/ui/view/:object', async (req, res) => {
261
- ctx.logger.debug('Get UI view request', { object: req.params.object, type: req.query.type });
724
+ },
725
+ 'get_ui_view': async (req, res) => {
726
+ const viewType = req.query.type || 'list';
727
+ ctx.logger.debug('UI view request', { object: req.params.object, viewType });
262
728
  try {
263
- const viewType = req.query.type || 'list';
264
729
  const view = await p.getUiView({ object: req.params.object, type: viewType });
265
730
  res.json(view);
266
731
  }
267
732
  catch (e) {
268
- ctx.logger.warn('UI view not found', { object: req.params.object, error: e.message });
733
+ ctx.logger.warn('UI view not found', { object: req.params.object });
269
734
  res.status(404).json({ error: e.message });
270
735
  }
736
+ }
737
+ };
738
+ return handlerMap[endpointId];
739
+ }
740
+ /**
741
+ * Legacy route registration (fallback when API Registry is not available)
742
+ */
743
+ bindLegacyRoutes(protocol, ctx) {
744
+ const p = protocol;
745
+ ctx.logger.debug('Using legacy route registration');
746
+ ctx.logger.debug('Registering API routes');
747
+ this.server.get('/api/v1', async (req, res) => {
748
+ ctx.logger.debug('API discovery request');
749
+ res.json(await p.getDiscovery({}));
750
+ });
751
+ // Meta Protocol
752
+ this.server.get('/api/v1/meta', async (req, res) => {
753
+ ctx.logger.debug('Meta types request');
754
+ res.json(await p.getMetaTypes({}));
755
+ });
756
+ this.server.get('/api/v1/meta/:type', async (req, res) => {
757
+ ctx.logger.debug('Meta items request', { type: req.params.type });
758
+ res.json(await p.getMetaItems({ type: req.params.type }));
759
+ });
760
+ // Data Protocol
761
+ this.server.get('/api/v1/data/:object', async (req, res) => {
762
+ ctx.logger.debug('Data find request', { object: req.params.object, query: req.query });
763
+ try {
764
+ const result = await p.findData({ object: req.params.object, query: req.query });
765
+ ctx.logger.debug('Data find completed', { object: req.params.object, count: result?.records?.length ?? 0 });
766
+ res.json(result);
767
+ }
768
+ catch (e) {
769
+ ctx.logger.error('Data find failed', e, { object: req.params.object });
770
+ res.status(400).json({ error: e.message });
771
+ }
772
+ });
773
+ this.server.get('/api/v1/data/:object/:id', async (req, res) => {
774
+ ctx.logger.debug('Data get request', { object: req.params.object, id: req.params.id });
775
+ try {
776
+ const result = await p.getData({ object: req.params.object, id: req.params.id });
777
+ ctx.logger.debug('Data get completed', { object: req.params.object, id: req.params.id });
778
+ res.json(result);
779
+ }
780
+ catch (e) {
781
+ ctx.logger.warn('Data get failed - not found', { object: req.params.object, id: req.params.id });
782
+ res.status(404).json({ error: e.message });
783
+ }
784
+ });
785
+ this.server.post('/api/v1/data/:object', async (req, res) => {
786
+ ctx.logger.debug('Data create request', { object: req.params.object });
787
+ try {
788
+ const result = await p.createData({ object: req.params.object, data: req.body });
789
+ ctx.logger.info('Data created', { object: req.params.object, id: result?.id });
790
+ res.status(201).json(result);
791
+ }
792
+ catch (e) {
793
+ ctx.logger.error('Data create failed', e, { object: req.params.object });
794
+ res.status(400).json({ error: e.message });
795
+ }
796
+ });
797
+ this.server.patch('/api/v1/data/:object/:id', async (req, res) => {
798
+ ctx.logger.debug('Data update request', { object: req.params.object, id: req.params.id });
799
+ try {
800
+ const result = await p.updateData({ object: req.params.object, id: req.params.id, data: req.body });
801
+ ctx.logger.info('Data updated', { object: req.params.object, id: req.params.id });
802
+ res.json(result);
803
+ }
804
+ catch (e) {
805
+ ctx.logger.error('Data update failed', e, { object: req.params.object, id: req.params.id });
806
+ res.status(400).json({ error: e.message });
807
+ }
808
+ });
809
+ this.server.delete('/api/v1/data/:object/:id', async (req, res) => {
810
+ ctx.logger.debug('Data delete request', { object: req.params.object, id: req.params.id });
811
+ try {
812
+ const result = await p.deleteData({ object: req.params.object, id: req.params.id });
813
+ ctx.logger.info('Data deleted', { object: req.params.object, id: req.params.id, success: result?.success });
814
+ res.json(result);
815
+ }
816
+ catch (e) {
817
+ ctx.logger.error('Data delete failed', e, { object: req.params.object, id: req.params.id });
818
+ res.status(400).json({ error: e.message });
819
+ }
820
+ });
821
+ // UI Protocol
822
+ this.server.get('/api/v1/ui/view/:object', async (req, res) => {
823
+ const viewType = (req.query.type) || 'list';
824
+ const qt = Array.isArray(viewType) ? viewType[0] : viewType;
825
+ ctx.logger.debug('UI view request', { object: req.params.object, viewType: qt });
826
+ try {
827
+ res.json(await p.getUiView({ object: req.params.object, type: qt }));
828
+ }
829
+ catch (e) {
830
+ ctx.logger.warn('UI view not found', { object: req.params.object, viewType: qt });
831
+ res.status(404).json({ error: e.message });
832
+ }
833
+ });
834
+ // Batch Operations
835
+ this.server.post('/api/v1/data/:object/batch', async (req, res) => {
836
+ ctx.logger.info('Batch operation request', {
837
+ object: req.params.object,
838
+ operation: req.body?.operation,
839
+ hasBody: !!req.body,
840
+ bodyType: typeof req.body,
841
+ bodyKeys: req.body ? Object.keys(req.body) : []
271
842
  });
272
- ctx.logger.info('All API routes registered');
273
- }
274
- // Start server on kernel:ready hook
275
- ctx.hook('kernel:ready', async () => {
276
- const port = this.options.port || 3000;
277
- ctx.logger.info('Starting HTTP server', { port });
278
- await this.server.listen(port);
279
- ctx.logger.info('HTTP server started successfully', {
280
- port,
281
- url: `http://localhost:${port}`
843
+ try {
844
+ const result = await p.batchData({ object: req.params.object, request: req.body });
845
+ ctx.logger.info('Batch operation completed', {
846
+ object: req.params.object,
847
+ operation: req.body?.operation,
848
+ total: result.total,
849
+ succeeded: result.succeeded,
850
+ failed: result.failed
851
+ });
852
+ res.json(result);
853
+ }
854
+ catch (e) {
855
+ ctx.logger.error('Batch operation failed', e, { object: req.params.object });
856
+ res.status(400).json({ error: e.message });
857
+ }
858
+ });
859
+ this.server.post('/api/v1/data/:object/createMany', async (req, res) => {
860
+ ctx.logger.debug('Create many request', { object: req.params.object, count: req.body?.length });
861
+ try {
862
+ const result = await p.createManyData({ object: req.params.object, records: req.body || [] });
863
+ ctx.logger.info('Create many completed', { object: req.params.object, count: result.records?.length ?? 0 });
864
+ res.status(201).json(result);
865
+ }
866
+ catch (e) {
867
+ ctx.logger.error('Create many failed', e, { object: req.params.object });
868
+ res.status(400).json({ error: e.message });
869
+ }
870
+ });
871
+ this.server.post('/api/v1/data/:object/updateMany', async (req, res) => {
872
+ ctx.logger.debug('Update many request', { object: req.params.object, count: req.body?.records?.length });
873
+ try {
874
+ const result = await p.updateManyData({ object: req.params.object, records: req.body?.records, options: req.body?.options });
875
+ ctx.logger.info('Update many completed', {
876
+ object: req.params.object,
877
+ total: result.total,
878
+ succeeded: result.succeeded,
879
+ failed: result.failed
880
+ });
881
+ res.json(result);
882
+ }
883
+ catch (e) {
884
+ ctx.logger.error('Update many failed', e, { object: req.params.object });
885
+ res.status(400).json({ error: e.message });
886
+ }
887
+ });
888
+ this.server.post('/api/v1/data/:object/deleteMany', async (req, res) => {
889
+ ctx.logger.debug('Delete many request', { object: req.params.object, count: req.body?.ids?.length });
890
+ try {
891
+ const result = await p.deleteManyData({ object: req.params.object, ids: req.body?.ids, options: req.body?.options });
892
+ ctx.logger.info('Delete many completed', {
893
+ object: req.params.object,
894
+ total: result.total,
895
+ succeeded: result.succeeded,
896
+ failed: result.failed
897
+ });
898
+ res.json(result);
899
+ }
900
+ catch (e) {
901
+ ctx.logger.error('Delete many failed', e, { object: req.params.object });
902
+ res.status(400).json({ error: e.message });
903
+ }
904
+ });
905
+ // Enhanced Metadata Route with ETag Support
906
+ this.server.get('/api/v1/meta/:type/:name', async (req, res) => {
907
+ ctx.logger.debug('Meta item request with cache support', {
908
+ type: req.params.type,
909
+ name: req.params.name,
910
+ ifNoneMatch: req.headers['if-none-match']
282
911
  });
912
+ try {
913
+ const cacheRequest = this.createCacheRequest(req.headers);
914
+ const result = await p.getMetaItemCached({
915
+ type: req.params.type,
916
+ name: req.params.name,
917
+ cacheRequest
918
+ });
919
+ if (result.notModified) {
920
+ ctx.logger.debug('Meta item not modified (304)', { type: req.params.type, name: req.params.name });
921
+ res.status(304).send('');
922
+ }
923
+ else {
924
+ // Set cache headers
925
+ if (result.etag) {
926
+ const etagValue = result.etag.weak ? `W/"${result.etag.value}"` : `"${result.etag.value}"`;
927
+ res.header('ETag', etagValue);
928
+ }
929
+ if (result.lastModified) {
930
+ res.header('Last-Modified', new Date(result.lastModified).toUTCString());
931
+ }
932
+ if (result.cacheControl) {
933
+ const directives = result.cacheControl.directives.join(', ');
934
+ const maxAge = result.cacheControl.maxAge ? `, max-age=${result.cacheControl.maxAge}` : '';
935
+ res.header('Cache-Control', directives + maxAge);
936
+ }
937
+ ctx.logger.debug('Meta item returned with cache headers', {
938
+ type: req.params.type,
939
+ name: req.params.name,
940
+ etag: result.etag?.value
941
+ });
942
+ res.json(result.data);
943
+ }
944
+ }
945
+ catch (e) {
946
+ ctx.logger.warn('Meta item not found', { type: req.params.type, name: req.params.name });
947
+ res.status(404).json({ error: e.message });
948
+ }
283
949
  });
950
+ ctx.logger.info('All legacy API routes registered');
284
951
  }
285
952
  /**
286
953
  * Destroy phase - Stop server