@objectstack/plugin-hono-server 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/src/hono-plugin.ts
CHANGED
|
@@ -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 = '
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
230
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
303
|
-
ctx.logger.
|
|
304
|
-
|
|
305
|
-
|
|
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
|
/**
|