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