@objectstack/plugin-hono-server 1.0.1 → 1.0.2

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.
@@ -6,6 +6,7 @@ import {
6
6
  RestServerConfig,
7
7
  } from '@objectstack/spec/api';
8
8
  import { HonoHttpServer } from './adapter';
9
+ import { createHonoApp } from '@objectstack/hono';
9
10
 
10
11
  export interface HonoPluginOptions {
11
12
  port?: number;
@@ -69,53 +70,32 @@ export class HonoServerPlugin implements Plugin {
69
70
  ctx.logger.info('HTTP server service registered', { serviceName: 'http-server' });
70
71
  }
71
72
 
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
-
82
73
  /**
83
74
  * Start phase - Bind routes and start listening
84
75
  */
85
76
  start = async (ctx: PluginContext) => {
86
77
  ctx.logger.debug('Starting Hono server plugin');
87
78
 
88
- // Get protocol implementation instance
89
- let protocol: ObjectStackProtocol | null = null;
90
-
91
- try {
92
- protocol = ctx.getService<ObjectStackProtocol>('protocol');
93
- ctx.logger.debug('Protocol service found');
94
- } catch (e) {
95
- ctx.logger.warn('Protocol service not found, skipping protocol routes');
96
- }
97
-
98
- // Try to get API Registry
99
- let apiRegistry: ApiRegistry | null = null;
79
+ // Use Standard ObjectStack Runtime Hono App
100
80
  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
108
- if (protocol) {
109
- if (apiRegistry && this.options.registerStandardEndpoints) {
110
- this.registerStandardEndpointsToRegistry(apiRegistry, ctx);
111
- }
81
+ const kernel = ctx.getKernel();
82
+ const config = this.options.restConfig || {};
83
+ // Calculate prefix similar to before
84
+ const apiVersion = config.api?.version || 'v1';
85
+ const basePath = config.api?.basePath || '/api';
86
+ const apiPath = config.api?.apiPath || `${basePath}/${apiVersion}`;
87
+
88
+ const app = createHonoApp({
89
+ kernel,
90
+ prefix: apiPath // Use the calculated path
91
+ });
92
+
93
+ ctx.logger.info('Mounting ObjectStack Runtime App', { prefix: apiPath });
94
+ // Use the mount method we added to HonoHttpServer
95
+ this.server.mount('/', app as any);
112
96
 
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
- }
97
+ } catch (e: any) {
98
+ ctx.logger.error('Failed to create standard Hono app', e);
119
99
  }
120
100
 
121
101
  // Start server on kernel:ready hook
@@ -131,1026 +111,6 @@ export class HonoServerPlugin implements Plugin {
131
111
  });
132
112
  }
133
113
 
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';
164
-
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
- // Analytics Endpoints
511
- endpoints.push(
512
- {
513
- id: 'analytics_query',
514
- method: 'POST',
515
- path: `${apiPath}/analytics/query`,
516
- summary: 'Analytics Query',
517
- description: 'Execute analytics query',
518
- priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
519
- },
520
- {
521
- id: 'get_analytics_meta',
522
- method: 'GET',
523
- path: `${apiPath}/analytics/meta`,
524
- summary: 'Analytics Metadata',
525
- description: 'Get analytics cubes definitions',
526
- priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
527
- }
528
- );
529
-
530
- // Automation Endpoints
531
- endpoints.push(
532
- {
533
- id: 'automation_trigger',
534
- method: 'POST',
535
- path: `${apiPath}/automation/trigger/:trigger`,
536
- summary: 'Trigger Automation',
537
- description: 'Trigger a named automation',
538
- parameters: [{
539
- name: 'trigger',
540
- in: 'path',
541
- required: true,
542
- schema: { type: 'string' }
543
- }],
544
- priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
545
- }
546
- );
547
-
548
- // Hub Endpoints
549
- endpoints.push(
550
- {
551
- id: 'hub_list_spaces',
552
- method: 'GET',
553
- path: `${apiPath}/hub/spaces`,
554
- summary: 'List Spaces',
555
- description: 'List all Hub spaces',
556
- priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
557
- },
558
- {
559
- id: 'hub_create_space',
560
- method: 'POST',
561
- path: `${apiPath}/hub/spaces`,
562
- summary: 'Create Space',
563
- description: 'Create a new Hub space',
564
- priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
565
- },
566
- {
567
- id: 'hub_install_plugin',
568
- method: 'POST',
569
- path: `${apiPath}/hub/plugins/install`,
570
- summary: 'Install Plugin',
571
- description: 'Install a plugin from marketplace',
572
- priority: HonoServerPlugin.DISCOVERY_ENDPOINT_PRIORITY
573
- }
574
- );
575
-
576
- // Register the API in the registry
577
- const apiEntry: ApiRegistryEntryInput = {
578
- id: 'objectstack_core_api',
579
- name: 'ObjectStack Core API',
580
- type: 'rest',
581
- version: apiVersion,
582
- basePath: apiPath,
583
- description: 'Standard ObjectStack CRUD and metadata API',
584
- endpoints,
585
- metadata: {
586
- owner: 'objectstack',
587
- status: 'active',
588
- tags: ['core', 'crud', 'metadata']
589
- }
590
- };
591
-
592
- try {
593
- registry.registerApi(apiEntry);
594
- ctx.logger.info('Standard ObjectStack endpoints registered to API Registry', {
595
- endpointCount: endpoints.length
596
- });
597
- } catch (error: any) {
598
- ctx.logger.error('Failed to register standard endpoints', error);
599
- }
600
- }
601
-
602
- /**
603
- * Bind HTTP routes from API Registry
604
- */
605
- private bindRoutesFromRegistry(registry: ApiRegistry, protocol: ObjectStackProtocol, ctx: PluginContext) {
606
- const apiRegistry = registry.getRegistry();
607
-
608
- ctx.logger.debug('Binding routes from API Registry', {
609
- totalApis: apiRegistry.totalApis,
610
- totalEndpoints: apiRegistry.totalEndpoints
611
- });
612
-
613
- // Get all endpoints sorted by priority (highest first)
614
- const allEndpoints: Array<{
615
- api: string;
616
- endpoint: any;
617
- }> = [];
618
-
619
- for (const api of apiRegistry.apis) {
620
- for (const endpoint of api.endpoints) {
621
- allEndpoints.push({ api: api.id, endpoint });
622
- }
623
- }
624
-
625
- // Sort by priority (highest first)
626
- allEndpoints.sort((a, b) =>
627
- (b.endpoint.priority || HonoServerPlugin.DEFAULT_ENDPOINT_PRIORITY) -
628
- (a.endpoint.priority || HonoServerPlugin.DEFAULT_ENDPOINT_PRIORITY)
629
- );
630
-
631
- // Bind routes
632
- for (const { endpoint } of allEndpoints) {
633
- this.bindEndpoint(endpoint, protocol, ctx);
634
- }
635
-
636
- ctx.logger.info('Routes bound from API Registry', {
637
- totalRoutes: allEndpoints.length
638
- });
639
- }
640
-
641
- /**
642
- * Bind a single endpoint to the HTTP server
643
- */
644
- private bindEndpoint(endpoint: any, protocol: ObjectStackProtocol, ctx: PluginContext) {
645
- const method = endpoint.method || 'GET';
646
- const path = endpoint.path;
647
- const id = endpoint.id;
648
-
649
- // Map endpoint ID to protocol method
650
- const handler = this.createHandlerForEndpoint(id, protocol, ctx);
651
-
652
- if (!handler) {
653
- ctx.logger.warn('No handler found for endpoint', { id, method, path });
654
- return;
655
- }
656
-
657
- // Register route based on method
658
- switch (method.toUpperCase()) {
659
- case 'GET':
660
- this.server.get(path, handler);
661
- break;
662
- case 'POST':
663
- this.server.post(path, handler);
664
- break;
665
- case 'PATCH':
666
- this.server.patch(path, handler);
667
- break;
668
- case 'PUT':
669
- this.server.put(path, handler);
670
- break;
671
- case 'DELETE':
672
- this.server.delete(path, handler);
673
- break;
674
- default:
675
- ctx.logger.warn('Unsupported HTTP method', { method, path });
676
- }
677
-
678
- ctx.logger.debug('Route bound', { method, path, endpoint: id });
679
- }
680
-
681
- /**
682
- * Create a route handler for an endpoint
683
- */
684
- private createHandlerForEndpoint(endpointId: string, protocol: ObjectStackProtocol, ctx: PluginContext) {
685
- const p = protocol;
686
-
687
- // Map endpoint IDs to protocol methods
688
- const handlerMap: Record<string, any> = {
689
- 'get_discovery': async (req: any, res: any) => {
690
- ctx.logger.debug('API discovery request');
691
- res.json(await p.getDiscovery({}));
692
- },
693
- 'get_meta_types': async (req: any, res: any) => {
694
- ctx.logger.debug('Meta types request');
695
- res.json(await p.getMetaTypes({}));
696
- },
697
- 'get_meta_items': async (req: any, res: any) => {
698
- ctx.logger.debug('Meta items request', { type: req.params.type });
699
- res.json(await p.getMetaItems({ type: req.params.type }));
700
- },
701
- 'get_meta_item_cached': async (req: any, res: any) => {
702
- ctx.logger.debug('Meta item cached request', {
703
- type: req.params.type,
704
- name: req.params.name
705
- });
706
- try {
707
- const result = await p.getMetaItemCached({
708
- type: req.params.type,
709
- name: req.params.name,
710
- cacheRequest: this.createCacheRequest(req.headers)
711
- });
712
-
713
- if (result.notModified) {
714
- res.status(304).send('');
715
- } else {
716
- // Set cache headers
717
- if (result.etag) {
718
- const etagValue = result.etag.weak ? `W/"${result.etag.value}"` : `"${result.etag.value}"`;
719
- res.header('ETag', etagValue);
720
- }
721
- if (result.lastModified) {
722
- res.header('Last-Modified', new Date(result.lastModified).toUTCString());
723
- }
724
- if (result.cacheControl) {
725
- const directives = result.cacheControl.directives.join(', ');
726
- const maxAge = result.cacheControl.maxAge ? `, max-age=${result.cacheControl.maxAge}` : '';
727
- res.header('Cache-Control', directives + maxAge);
728
- }
729
- res.json(result.data);
730
- }
731
- } catch (e: any) {
732
- ctx.logger.warn('Meta item not found', { type: req.params.type, name: req.params.name });
733
- res.status(404).json({ error: e.message });
734
- }
735
- },
736
- 'find_data': async (req: any, res: any) => {
737
- ctx.logger.debug('Data find request', { object: req.params.object });
738
- try {
739
- const result = await p.findData({ object: req.params.object, query: req.query as any });
740
- ctx.logger.debug('Data find completed', { object: req.params.object, count: result?.records?.length ?? 0 });
741
- res.json(result);
742
- } catch (e: any) {
743
- ctx.logger.error('Data find failed', e, { object: req.params.object });
744
- res.status(400).json({ error: e.message });
745
- }
746
- },
747
- 'get_data': async (req: any, res: any) => {
748
- ctx.logger.debug('Data get request', { object: req.params.object, id: req.params.id });
749
- try {
750
- const result = await p.getData({ object: req.params.object, id: req.params.id });
751
- res.json(result);
752
- } catch (e: any) {
753
- ctx.logger.warn('Data get failed', { object: req.params.object, id: req.params.id });
754
- res.status(404).json({ error: e.message });
755
- }
756
- },
757
- 'create_data': async (req: any, res: any) => {
758
- ctx.logger.debug('Data create request', { object: req.params.object });
759
- try {
760
- const result = await p.createData({ object: req.params.object, data: req.body });
761
- ctx.logger.info('Data created', { object: req.params.object, id: result?.id });
762
- res.status(201).json(result);
763
- } catch (e: any) {
764
- ctx.logger.error('Data create failed', e, { object: req.params.object });
765
- res.status(400).json({ error: e.message });
766
- }
767
- },
768
- 'update_data': async (req: any, res: any) => {
769
- ctx.logger.debug('Data update request', { object: req.params.object, id: req.params.id });
770
- try {
771
- const result = await p.updateData({ object: req.params.object, id: req.params.id, data: req.body });
772
- ctx.logger.info('Data updated', { object: req.params.object, id: req.params.id });
773
- res.json(result);
774
- } catch (e: any) {
775
- ctx.logger.error('Data update failed', e, { object: req.params.object, id: req.params.id });
776
- res.status(400).json({ error: e.message });
777
- }
778
- },
779
- 'delete_data': async (req: any, res: any) => {
780
- ctx.logger.debug('Data delete request', { object: req.params.object, id: req.params.id });
781
- try {
782
- const result = await p.deleteData({ object: req.params.object, id: req.params.id });
783
- ctx.logger.info('Data deleted', { object: req.params.object, id: req.params.id });
784
- res.json(result);
785
- } catch (e: any) {
786
- ctx.logger.error('Data delete failed', e, { object: req.params.object, id: req.params.id });
787
- res.status(400).json({ error: e.message });
788
- }
789
- },
790
- 'batch_data': async (req: any, res: any) => {
791
- ctx.logger.info('Batch operation request', { object: req.params.object });
792
- try {
793
- const result = await p.batchData({ object: req.params.object, request: req.body });
794
- ctx.logger.info('Batch operation completed', {
795
- object: req.params.object,
796
- total: result.total,
797
- succeeded: result.succeeded,
798
- failed: result.failed
799
- });
800
- res.json(result);
801
- } catch (e: any) {
802
- ctx.logger.error('Batch operation failed', e, { object: req.params.object });
803
- res.status(400).json({ error: e.message });
804
- }
805
- },
806
- 'create_many_data': async (req: any, res: any) => {
807
- ctx.logger.debug('Create many request', { object: req.params.object });
808
- try {
809
- const result = await p.createManyData({ object: req.params.object, records: req.body || [] });
810
- ctx.logger.info('Create many completed', { object: req.params.object, count: result.records?.length ?? 0 });
811
- res.status(201).json(result);
812
- } catch (e: any) {
813
- ctx.logger.error('Create many failed', e, { object: req.params.object });
814
- res.status(400).json({ error: e.message });
815
- }
816
- },
817
- 'update_many_data': async (req: any, res: any) => {
818
- ctx.logger.debug('Update many request', { object: req.params.object });
819
- try {
820
- const result = await p.updateManyData({
821
- object: req.params.object,
822
- records: req.body?.records,
823
- options: req.body?.options
824
- });
825
- ctx.logger.info('Update many completed', {
826
- object: req.params.object,
827
- total: result.total,
828
- succeeded: result.succeeded,
829
- failed: result.failed
830
- });
831
- res.json(result);
832
- } catch (e: any) {
833
- ctx.logger.error('Update many failed', e, { object: req.params.object });
834
- res.status(400).json({ error: e.message });
835
- }
836
- },
837
- 'delete_many_data': async (req: any, res: any) => {
838
- ctx.logger.debug('Delete many request', { object: req.params.object });
839
- try {
840
- const result = await p.deleteManyData({
841
- object: req.params.object,
842
- ids: req.body?.ids,
843
- options: req.body?.options
844
- });
845
- ctx.logger.info('Delete many completed', {
846
- object: req.params.object,
847
- total: result.total,
848
- succeeded: result.succeeded,
849
- failed: result.failed
850
- });
851
- res.json(result);
852
- } catch (e: any) {
853
- ctx.logger.error('Delete many failed', e, { object: req.params.object });
854
- res.status(400).json({ error: e.message });
855
- }
856
- },
857
- 'get_ui_view': async (req: any, res: any) => {
858
- const viewType = (req.query.type as 'list' | 'form') || 'list';
859
- ctx.logger.debug('UI view request', { object: req.params.object, viewType });
860
- try {
861
- const view = await p.getUiView({ object: req.params.object, type: viewType });
862
- res.json(view);
863
- } catch (e: any) {
864
- ctx.logger.warn('UI view not found', { object: req.params.object });
865
- res.status(404).json({ error: e.message });
866
- }
867
- },
868
- // Analytics
869
- 'analytics_query': async (req: any, res: any) => {
870
- ctx.logger.info('Analytics query request');
871
- try {
872
- const result = await p.analyticsQuery(req.body);
873
- res.json(result);
874
- } catch (e: any) {
875
- ctx.logger.error('Analytics query failed', e);
876
- res.status(400).json({ error: e.message });
877
- }
878
- },
879
- 'get_analytics_meta': async (req: any, res: any) => {
880
- ctx.logger.info('Analytics meta request');
881
- try {
882
- const result = await p.getAnalyticsMeta({});
883
- res.json(result);
884
- } catch (e: any) {
885
- ctx.logger.error('Analytics meta failed', e);
886
- res.status(500).json({ error: e.message });
887
- }
888
- },
889
- // Automation
890
- 'automation_trigger': async (req: any, res: any) => {
891
- const trigger = req.params.trigger;
892
- ctx.logger.info('Automation trigger request', { trigger });
893
- try {
894
- const result = await p.triggerAutomation({ trigger, payload: req.body });
895
- res.json(result);
896
- } catch (e: any) {
897
- ctx.logger.error('Automation trigger failed', e, { trigger });
898
- res.status(500).json({ error: e.message });
899
- }
900
- },
901
- // Hub
902
- 'hub_list_spaces': async (req: any, res: any) => {
903
- try {
904
- const result = await p.listSpaces({ ...req.query });
905
- res.json(result);
906
- } catch (e: any) {
907
- res.status(500).json({ error: e.message });
908
- }
909
- },
910
- 'hub_create_space': async (req: any, res: any) => {
911
- try {
912
- const result = await p.createSpace(req.body);
913
- res.status(201).json(result);
914
- } catch (e: any) {
915
- res.status(500).json({ error: e.message });
916
- }
917
- },
918
- 'hub_install_plugin': async (req: any, res: any) => {
919
- const spaceId = req.params.space_id;
920
- try {
921
- const result = await p.installPlugin({ spaceId, ...req.body });
922
- res.json(result);
923
- } catch (e: any) {
924
- res.status(500).json({ error: e.message });
925
- }
926
- }
927
- };
928
-
929
- return handlerMap[endpointId];
930
- }
931
-
932
- /**
933
- * Legacy route registration (fallback when API Registry is not available)
934
- */
935
- private bindLegacyRoutes(protocol: ObjectStackProtocol, ctx: PluginContext) {
936
- const p = protocol;
937
-
938
- ctx.logger.debug('Using legacy route registration');
939
-
940
- ctx.logger.debug('Registering API routes');
941
-
942
- this.server.get('/api/v1', async (req, res) => {
943
- ctx.logger.debug('API discovery request');
944
- res.json(await p.getDiscovery({}));
945
- });
946
-
947
- // Meta Protocol
948
- this.server.get('/api/v1/meta', async (req, res) => {
949
- ctx.logger.debug('Meta types request');
950
- res.json(await p.getMetaTypes({}));
951
- });
952
- this.server.get('/api/v1/meta/:type', async (req, res) => {
953
- ctx.logger.debug('Meta items request', { type: req.params.type });
954
- res.json(await p.getMetaItems({ type: req.params.type }));
955
- });
956
-
957
- // Data Protocol
958
- this.server.get('/api/v1/data/:object', async (req, res) => {
959
- ctx.logger.debug('Data find request', { object: req.params.object, query: req.query });
960
- try {
961
- const result = await p.findData({ object: req.params.object, query: req.query as any });
962
- ctx.logger.debug('Data find completed', { object: req.params.object, count: result?.records?.length ?? 0 });
963
- res.json(result);
964
- }
965
- catch(e:any) {
966
- ctx.logger.error('Data find failed', e, { object: req.params.object });
967
- res.status(400).json({error:e.message});
968
- }
969
- });
970
- this.server.get('/api/v1/data/:object/:id', async (req, res) => {
971
- ctx.logger.debug('Data get request', { object: req.params.object, id: req.params.id });
972
- try {
973
- const result = await p.getData({ object: req.params.object, id: req.params.id });
974
- ctx.logger.debug('Data get completed', { object: req.params.object, id: req.params.id });
975
- res.json(result);
976
- }
977
- catch(e:any) {
978
- ctx.logger.warn('Data get failed - not found', { object: req.params.object, id: req.params.id });
979
- res.status(404).json({error:e.message});
980
- }
981
- });
982
- this.server.post('/api/v1/data/:object', async (req, res) => {
983
- ctx.logger.debug('Data create request', { object: req.params.object });
984
- try {
985
- const result = await p.createData({ object: req.params.object, data: req.body });
986
- ctx.logger.info('Data created', { object: req.params.object, id: result?.id });
987
- res.status(201).json(result);
988
- }
989
- catch(e:any) {
990
- ctx.logger.error('Data create failed', e, { object: req.params.object });
991
- res.status(400).json({error:e.message});
992
- }
993
- });
994
- this.server.patch('/api/v1/data/:object/:id', async (req, res) => {
995
- ctx.logger.debug('Data update request', { object: req.params.object, id: req.params.id });
996
- try {
997
- const result = await p.updateData({ object: req.params.object, id: req.params.id, data: req.body });
998
- ctx.logger.info('Data updated', { object: req.params.object, id: req.params.id });
999
- res.json(result);
1000
- }
1001
- catch(e:any) {
1002
- ctx.logger.error('Data update failed', e, { object: req.params.object, id: req.params.id });
1003
- res.status(400).json({error:e.message});
1004
- }
1005
- });
1006
- this.server.delete('/api/v1/data/:object/:id', async (req, res) => {
1007
- ctx.logger.debug('Data delete request', { object: req.params.object, id: req.params.id });
1008
- try {
1009
- const result = await p.deleteData({ object: req.params.object, id: req.params.id });
1010
- ctx.logger.info('Data deleted', { object: req.params.object, id: req.params.id, success: result?.success });
1011
- res.json(result);
1012
- }
1013
- catch(e:any) {
1014
- ctx.logger.error('Data delete failed', e, { object: req.params.object, id: req.params.id });
1015
- res.status(400).json({error:e.message});
1016
- }
1017
- });
1018
-
1019
- // UI Protocol
1020
- this.server.get('/api/v1/ui/view/:object', async (req, res) => {
1021
- const viewType = (req.query.type) || 'list';
1022
- const qt = Array.isArray(viewType) ? viewType[0] : viewType;
1023
- ctx.logger.debug('UI view request', { object: req.params.object, viewType: qt });
1024
- try {
1025
- res.json(await p.getUiView({ object: req.params.object, type: qt as any }));
1026
- }
1027
- catch(e:any) {
1028
- ctx.logger.warn('UI view not found', { object: req.params.object, viewType: qt });
1029
- res.status(404).json({error:e.message});
1030
- }
1031
- });
1032
-
1033
- // Batch Operations
1034
- this.server.post('/api/v1/data/:object/batch', async (req, res) => {
1035
- ctx.logger.info('Batch operation request', {
1036
- object: req.params.object,
1037
- operation: req.body?.operation,
1038
- hasBody: !!req.body,
1039
- bodyType: typeof req.body,
1040
- bodyKeys: req.body ? Object.keys(req.body) : []
1041
- });
1042
- try {
1043
- const result = await p.batchData({ object: req.params.object, request: req.body });
1044
- ctx.logger.info('Batch operation completed', {
1045
- object: req.params.object,
1046
- operation: req.body?.operation,
1047
- total: result.total,
1048
- succeeded: result.succeeded,
1049
- failed: result.failed
1050
- });
1051
- res.json(result);
1052
- } catch (e: any) {
1053
- ctx.logger.error('Batch operation failed', e, { object: req.params.object });
1054
- res.status(400).json({ error: e.message });
1055
- }
1056
- });
1057
-
1058
- this.server.post('/api/v1/data/:object/createMany', async (req, res) => {
1059
- ctx.logger.debug('Create many request', { object: req.params.object, count: req.body?.length });
1060
- try {
1061
- const result = await p.createManyData({ object: req.params.object, records: req.body || [] });
1062
- ctx.logger.info('Create many completed', { object: req.params.object, count: result.records?.length ?? 0 });
1063
- res.status(201).json(result);
1064
- } catch (e: any) {
1065
- ctx.logger.error('Create many failed', e, { object: req.params.object });
1066
- res.status(400).json({ error: e.message });
1067
- }
1068
- });
1069
-
1070
- this.server.post('/api/v1/data/:object/updateMany', async (req, res) => {
1071
- ctx.logger.debug('Update many request', { object: req.params.object, count: req.body?.records?.length });
1072
- try {
1073
- const result = await p.updateManyData({ object: req.params.object, records: req.body?.records, options: req.body?.options });
1074
- ctx.logger.info('Update many completed', {
1075
- object: req.params.object,
1076
- total: result.total,
1077
- succeeded: result.succeeded,
1078
- failed: result.failed
1079
- });
1080
- res.json(result);
1081
- } catch (e: any) {
1082
- ctx.logger.error('Update many failed', e, { object: req.params.object });
1083
- res.status(400).json({ error: e.message });
1084
- }
1085
- });
1086
-
1087
- this.server.post('/api/v1/data/:object/deleteMany', async (req, res) => {
1088
- ctx.logger.debug('Delete many request', { object: req.params.object, count: req.body?.ids?.length });
1089
- try {
1090
- const result = await p.deleteManyData({ object: req.params.object, ids: req.body?.ids, options: req.body?.options });
1091
- ctx.logger.info('Delete many completed', {
1092
- object: req.params.object,
1093
- total: result.total,
1094
- succeeded: result.succeeded,
1095
- failed: result.failed
1096
- });
1097
- res.json(result);
1098
- } catch (e: any) {
1099
- ctx.logger.error('Delete many failed', e, { object: req.params.object });
1100
- res.status(400).json({ error: e.message });
1101
- }
1102
- });
1103
-
1104
- // Enhanced Metadata Route with ETag Support
1105
- this.server.get('/api/v1/meta/:type/:name', async (req, res) => {
1106
- ctx.logger.debug('Meta item request with cache support', {
1107
- type: req.params.type,
1108
- name: req.params.name,
1109
- ifNoneMatch: req.headers['if-none-match']
1110
- });
1111
- try {
1112
- const cacheRequest = this.createCacheRequest(req.headers);
1113
-
1114
- const result = await p.getMetaItemCached({
1115
- type: req.params.type,
1116
- name: req.params.name,
1117
- cacheRequest
1118
- });
1119
-
1120
- if (result.notModified) {
1121
- ctx.logger.debug('Meta item not modified (304)', { type: req.params.type, name: req.params.name });
1122
- res.status(304).send('');
1123
- } else {
1124
- // Set cache headers
1125
- if (result.etag) {
1126
- const etagValue = result.etag.weak ? `W/"${result.etag.value}"` : `"${result.etag.value}"`;
1127
- res.header('ETag', etagValue);
1128
- }
1129
- if (result.lastModified) {
1130
- res.header('Last-Modified', new Date(result.lastModified).toUTCString());
1131
- }
1132
- if (result.cacheControl) {
1133
- const directives = result.cacheControl.directives.join(', ');
1134
- const maxAge = result.cacheControl.maxAge ? `, max-age=${result.cacheControl.maxAge}` : '';
1135
- res.header('Cache-Control', directives + maxAge);
1136
- }
1137
-
1138
- ctx.logger.debug('Meta item returned with cache headers', {
1139
- type: req.params.type,
1140
- name: req.params.name,
1141
- etag: result.etag?.value
1142
- });
1143
- res.json(result.data);
1144
- }
1145
- } catch (e: any) {
1146
- ctx.logger.warn('Meta item not found', { type: req.params.type, name: req.params.name });
1147
- res.status(404).json({ error: e.message });
1148
- }
1149
- });
1150
-
1151
- ctx.logger.info('All legacy API routes registered');
1152
- }
1153
-
1154
114
  /**
1155
115
  * Destroy phase - Stop server
1156
116
  */