@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.
- package/CHANGELOG.md +10 -0
- package/README.md +175 -0
- package/dist/hono-plugin.d.ts +43 -0
- package/dist/hono-plugin.js +792 -125
- package/package.json +8 -6
- package/src/hono-plugin.test.ts +190 -0
- package/src/hono-plugin.ts +878 -159
- package/tsconfig.json +2 -1
package/dist/hono-plugin.js
CHANGED
|
@@ -10,12 +10,18 @@ const adapter_1 = require("./adapter");
|
|
|
10
10
|
*/
|
|
11
11
|
class HonoServerPlugin {
|
|
12
12
|
name = 'com.objectstack.server.hono';
|
|
13
|
-
version = '
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
166
|
-
ctx.logger.debug('Create many request', { object: req.params.object
|
|
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
|
-
|
|
178
|
-
ctx.logger.debug('Update many request', { object: req.params.object
|
|
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({
|
|
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
|
-
|
|
195
|
-
ctx.logger.debug('Delete many request', { object: req.params.object
|
|
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({
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
ctx.logger.debug('
|
|
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
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|