@objectstack/runtime 1.0.2 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,518 +0,0 @@
1
- import { RouteManager } from './route-manager.js';
2
- /**
3
- * RestServer
4
- *
5
- * Provides automatic REST API endpoint generation for ObjectStack.
6
- * Generates standard RESTful CRUD endpoints, metadata endpoints, and batch operations
7
- * based on the configured protocol provider.
8
- *
9
- * Features:
10
- * - Automatic CRUD endpoint generation (GET, POST, PUT, PATCH, DELETE)
11
- * - Metadata API endpoints (/meta)
12
- * - Batch operation endpoints (/batch, /createMany, /updateMany, /deleteMany)
13
- * - Discovery endpoint
14
- * - Configurable path prefixes and patterns
15
- *
16
- * @example
17
- * const restServer = new RestServer(httpServer, protocolProvider, {
18
- * api: {
19
- * version: 'v1',
20
- * basePath: '/api'
21
- * },
22
- * crud: {
23
- * dataPrefix: '/data'
24
- * }
25
- * });
26
- *
27
- * restServer.registerRoutes();
28
- */
29
- export class RestServer {
30
- constructor(server, protocol, config = {}) {
31
- this.server = server;
32
- this.protocol = protocol;
33
- this.config = this.normalizeConfig(config);
34
- this.routeManager = new RouteManager(server);
35
- }
36
- /**
37
- * Normalize configuration with defaults
38
- */
39
- normalizeConfig(config) {
40
- const api = (config.api ?? {});
41
- const crud = (config.crud ?? {});
42
- const metadata = (config.metadata ?? {});
43
- const batch = (config.batch ?? {});
44
- const routes = (config.routes ?? {});
45
- return {
46
- api: {
47
- version: api.version ?? 'v1',
48
- basePath: api.basePath ?? '/api',
49
- apiPath: api.apiPath,
50
- enableCrud: api.enableCrud ?? true,
51
- enableMetadata: api.enableMetadata ?? true,
52
- enableBatch: api.enableBatch ?? true,
53
- enableDiscovery: api.enableDiscovery ?? true,
54
- documentation: api.documentation,
55
- responseFormat: api.responseFormat,
56
- },
57
- crud: {
58
- operations: crud.operations ?? {
59
- create: true,
60
- read: true,
61
- update: true,
62
- delete: true,
63
- list: true,
64
- },
65
- patterns: crud.patterns,
66
- dataPrefix: crud.dataPrefix ?? '/data',
67
- objectParamStyle: crud.objectParamStyle ?? 'path',
68
- },
69
- metadata: {
70
- prefix: metadata.prefix ?? '/meta',
71
- enableCache: metadata.enableCache ?? true,
72
- cacheTtl: metadata.cacheTtl ?? 3600,
73
- endpoints: metadata.endpoints ?? {
74
- types: true,
75
- items: true,
76
- item: true,
77
- schema: true,
78
- },
79
- },
80
- batch: {
81
- maxBatchSize: batch.maxBatchSize ?? 200,
82
- enableBatchEndpoint: batch.enableBatchEndpoint ?? true,
83
- operations: batch.operations ?? {
84
- createMany: true,
85
- updateMany: true,
86
- deleteMany: true,
87
- upsertMany: true,
88
- },
89
- defaultAtomic: batch.defaultAtomic ?? true,
90
- },
91
- routes: {
92
- includeObjects: routes.includeObjects,
93
- excludeObjects: routes.excludeObjects,
94
- nameTransform: routes.nameTransform ?? 'none',
95
- overrides: routes.overrides,
96
- },
97
- };
98
- }
99
- /**
100
- * Get the full API base path
101
- */
102
- getApiBasePath() {
103
- const { api } = this.config;
104
- return api.apiPath ?? `${api.basePath}/${api.version}`;
105
- }
106
- /**
107
- * Register all REST API routes
108
- */
109
- registerRoutes() {
110
- const basePath = this.getApiBasePath();
111
- // Discovery endpoint
112
- if (this.config.api.enableDiscovery) {
113
- this.registerDiscoveryEndpoints(basePath);
114
- }
115
- // Metadata endpoints
116
- if (this.config.api.enableMetadata) {
117
- this.registerMetadataEndpoints(basePath);
118
- }
119
- // CRUD endpoints
120
- if (this.config.api.enableCrud) {
121
- this.registerCrudEndpoints(basePath);
122
- }
123
- // Batch endpoints
124
- if (this.config.api.enableBatch) {
125
- this.registerBatchEndpoints(basePath);
126
- }
127
- }
128
- /**
129
- * Register discovery endpoints
130
- */
131
- registerDiscoveryEndpoints(basePath) {
132
- this.routeManager.register({
133
- method: 'GET',
134
- path: basePath,
135
- handler: async (req, res) => {
136
- try {
137
- const discovery = await this.protocol.getDiscovery({});
138
- res.json(discovery);
139
- }
140
- catch (error) {
141
- res.status(500).json({ error: error.message });
142
- }
143
- },
144
- metadata: {
145
- summary: 'Get API discovery information',
146
- tags: ['discovery'],
147
- },
148
- });
149
- }
150
- /**
151
- * Register metadata endpoints
152
- */
153
- registerMetadataEndpoints(basePath) {
154
- const { metadata } = this.config;
155
- const metaPath = `${basePath}${metadata.prefix}`;
156
- // GET /meta - List all metadata types
157
- if (metadata.endpoints.types !== false) {
158
- this.routeManager.register({
159
- method: 'GET',
160
- path: metaPath,
161
- handler: async (req, res) => {
162
- try {
163
- const types = await this.protocol.getMetaTypes({});
164
- res.json(types);
165
- }
166
- catch (error) {
167
- res.status(500).json({ error: error.message });
168
- }
169
- },
170
- metadata: {
171
- summary: 'List all metadata types',
172
- tags: ['metadata'],
173
- },
174
- });
175
- }
176
- // GET /meta/:type - List items of a type
177
- if (metadata.endpoints.items !== false) {
178
- this.routeManager.register({
179
- method: 'GET',
180
- path: `${metaPath}/:type`,
181
- handler: async (req, res) => {
182
- try {
183
- const items = await this.protocol.getMetaItems({ type: req.params.type });
184
- res.json(items);
185
- }
186
- catch (error) {
187
- res.status(404).json({ error: error.message });
188
- }
189
- },
190
- metadata: {
191
- summary: 'List metadata items of a type',
192
- tags: ['metadata'],
193
- },
194
- });
195
- }
196
- // GET /meta/:type/:name - Get specific item
197
- if (metadata.endpoints.item !== false) {
198
- this.routeManager.register({
199
- method: 'GET',
200
- path: `${metaPath}/:type/:name`,
201
- handler: async (req, res) => {
202
- try {
203
- // Check if cached version is available
204
- if (metadata.enableCache && this.protocol.getMetaItemCached) {
205
- const cacheRequest = {
206
- ifNoneMatch: req.headers['if-none-match'],
207
- ifModifiedSince: req.headers['if-modified-since'],
208
- };
209
- const result = await this.protocol.getMetaItemCached({
210
- type: req.params.type,
211
- name: req.params.name,
212
- cacheRequest
213
- });
214
- if (result.notModified) {
215
- res.status(304).send();
216
- return;
217
- }
218
- // Set cache headers
219
- if (result.etag) {
220
- const etagValue = result.etag.weak
221
- ? `W/"${result.etag.value}"`
222
- : `"${result.etag.value}"`;
223
- res.header('ETag', etagValue);
224
- }
225
- if (result.lastModified) {
226
- res.header('Last-Modified', new Date(result.lastModified).toUTCString());
227
- }
228
- if (result.cacheControl) {
229
- const directives = result.cacheControl.directives.join(', ');
230
- const maxAge = result.cacheControl.maxAge
231
- ? `, max-age=${result.cacheControl.maxAge}`
232
- : '';
233
- res.header('Cache-Control', directives + maxAge);
234
- }
235
- res.json(result.data);
236
- }
237
- else {
238
- // Non-cached version
239
- const item = await this.protocol.getMetaItem({ type: req.params.type, name: req.params.name });
240
- res.json(item);
241
- }
242
- }
243
- catch (error) {
244
- res.status(404).json({ error: error.message });
245
- }
246
- },
247
- metadata: {
248
- summary: 'Get specific metadata item',
249
- tags: ['metadata'],
250
- },
251
- });
252
- }
253
- // PUT /meta/:type/:name - Save metadata item
254
- // We always register this route, but return 501 if protocol doesn't support it
255
- // This makes it discoverable even if not implemented
256
- this.routeManager.register({
257
- method: 'PUT',
258
- path: `${metaPath}/:type/:name`,
259
- handler: async (req, res) => {
260
- try {
261
- if (!this.protocol.saveMetaItem) {
262
- res.status(501).json({ error: 'Save operation not supported by protocol implementation' });
263
- return;
264
- }
265
- const result = await this.protocol.saveMetaItem({
266
- type: req.params.type,
267
- name: req.params.name,
268
- item: req.body
269
- });
270
- res.json(result);
271
- }
272
- catch (error) {
273
- res.status(400).json({ error: error.message });
274
- }
275
- },
276
- metadata: {
277
- summary: 'Save specific metadata item',
278
- tags: ['metadata'],
279
- },
280
- });
281
- }
282
- /**
283
- * Register CRUD endpoints for data operations
284
- */
285
- registerCrudEndpoints(basePath) {
286
- const { crud } = this.config;
287
- const dataPath = `${basePath}${crud.dataPrefix}`;
288
- const operations = crud.operations;
289
- // GET /data/:object - List/query records
290
- if (operations.list) {
291
- this.routeManager.register({
292
- method: 'GET',
293
- path: `${dataPath}/:object`,
294
- handler: async (req, res) => {
295
- try {
296
- const result = await this.protocol.findData({
297
- object: req.params.object,
298
- query: req.query
299
- });
300
- res.json(result);
301
- }
302
- catch (error) {
303
- res.status(400).json({ error: error.message });
304
- }
305
- },
306
- metadata: {
307
- summary: 'Query records',
308
- tags: ['data', 'crud'],
309
- },
310
- });
311
- }
312
- // GET /data/:object/:id - Get single record
313
- if (operations.read) {
314
- this.routeManager.register({
315
- method: 'GET',
316
- path: `${dataPath}/:object/:id`,
317
- handler: async (req, res) => {
318
- try {
319
- const result = await this.protocol.getData({
320
- object: req.params.object,
321
- id: req.params.id
322
- });
323
- res.json(result);
324
- }
325
- catch (error) {
326
- res.status(404).json({ error: error.message });
327
- }
328
- },
329
- metadata: {
330
- summary: 'Get record by ID',
331
- tags: ['data', 'crud'],
332
- },
333
- });
334
- }
335
- // POST /data/:object - Create record
336
- if (operations.create) {
337
- this.routeManager.register({
338
- method: 'POST',
339
- path: `${dataPath}/:object`,
340
- handler: async (req, res) => {
341
- try {
342
- const result = await this.protocol.createData({
343
- object: req.params.object,
344
- data: req.body
345
- });
346
- res.status(201).json(result);
347
- }
348
- catch (error) {
349
- res.status(400).json({ error: error.message });
350
- }
351
- },
352
- metadata: {
353
- summary: 'Create record',
354
- tags: ['data', 'crud'],
355
- },
356
- });
357
- }
358
- // PATCH /data/:object/:id - Update record
359
- if (operations.update) {
360
- this.routeManager.register({
361
- method: 'PATCH',
362
- path: `${dataPath}/:object/:id`,
363
- handler: async (req, res) => {
364
- try {
365
- const result = await this.protocol.updateData({
366
- object: req.params.object,
367
- id: req.params.id,
368
- data: req.body
369
- });
370
- res.json(result);
371
- }
372
- catch (error) {
373
- res.status(400).json({ error: error.message });
374
- }
375
- },
376
- metadata: {
377
- summary: 'Update record',
378
- tags: ['data', 'crud'],
379
- },
380
- });
381
- }
382
- // DELETE /data/:object/:id - Delete record
383
- if (operations.delete) {
384
- this.routeManager.register({
385
- method: 'DELETE',
386
- path: `${dataPath}/:object/:id`,
387
- handler: async (req, res) => {
388
- try {
389
- const result = await this.protocol.deleteData({
390
- object: req.params.object,
391
- id: req.params.id
392
- });
393
- res.json(result);
394
- }
395
- catch (error) {
396
- res.status(400).json({ error: error.message });
397
- }
398
- },
399
- metadata: {
400
- summary: 'Delete record',
401
- tags: ['data', 'crud'],
402
- },
403
- });
404
- }
405
- }
406
- /**
407
- * Register batch operation endpoints
408
- */
409
- registerBatchEndpoints(basePath) {
410
- const { crud, batch } = this.config;
411
- const dataPath = `${basePath}${crud.dataPrefix}`;
412
- const operations = batch.operations;
413
- // POST /data/:object/batch - Generic batch endpoint
414
- if (batch.enableBatchEndpoint && this.protocol.batchData) {
415
- this.routeManager.register({
416
- method: 'POST',
417
- path: `${dataPath}/:object/batch`,
418
- handler: async (req, res) => {
419
- try {
420
- const result = await this.protocol.batchData({
421
- object: req.params.object,
422
- request: req.body
423
- });
424
- res.json(result);
425
- }
426
- catch (error) {
427
- res.status(400).json({ error: error.message });
428
- }
429
- },
430
- metadata: {
431
- summary: 'Batch operations',
432
- tags: ['data', 'batch'],
433
- },
434
- });
435
- }
436
- // POST /data/:object/createMany - Bulk create
437
- if (operations.createMany && this.protocol.createManyData) {
438
- this.routeManager.register({
439
- method: 'POST',
440
- path: `${dataPath}/:object/createMany`,
441
- handler: async (req, res) => {
442
- try {
443
- const result = await this.protocol.createManyData({
444
- object: req.params.object,
445
- records: req.body || []
446
- });
447
- res.status(201).json(result);
448
- }
449
- catch (error) {
450
- res.status(400).json({ error: error.message });
451
- }
452
- },
453
- metadata: {
454
- summary: 'Create multiple records',
455
- tags: ['data', 'batch'],
456
- },
457
- });
458
- }
459
- // POST /data/:object/updateMany - Bulk update
460
- if (operations.updateMany && this.protocol.updateManyData) {
461
- this.routeManager.register({
462
- method: 'POST',
463
- path: `${dataPath}/:object/updateMany`,
464
- handler: async (req, res) => {
465
- try {
466
- const result = await this.protocol.updateManyData({
467
- object: req.params.object,
468
- ...req.body
469
- });
470
- res.json(result);
471
- }
472
- catch (error) {
473
- res.status(400).json({ error: error.message });
474
- }
475
- },
476
- metadata: {
477
- summary: 'Update multiple records',
478
- tags: ['data', 'batch'],
479
- },
480
- });
481
- }
482
- // POST /data/:object/deleteMany - Bulk delete
483
- if (operations.deleteMany && this.protocol.deleteManyData) {
484
- this.routeManager.register({
485
- method: 'POST',
486
- path: `${dataPath}/:object/deleteMany`,
487
- handler: async (req, res) => {
488
- try {
489
- const result = await this.protocol.deleteManyData({
490
- object: req.params.object,
491
- ...req.body
492
- });
493
- res.json(result);
494
- }
495
- catch (error) {
496
- res.status(400).json({ error: error.message });
497
- }
498
- },
499
- metadata: {
500
- summary: 'Delete multiple records',
501
- tags: ['data', 'batch'],
502
- },
503
- });
504
- }
505
- }
506
- /**
507
- * Get the route manager
508
- */
509
- getRouteManager() {
510
- return this.routeManager;
511
- }
512
- /**
513
- * Get all registered routes
514
- */
515
- getRoutes() {
516
- return this.routeManager.getAll();
517
- }
518
- }
@@ -1,153 +0,0 @@
1
- import { RouteHandler, IHttpServer } from '@objectstack/core';
2
- import { System, Shared } from '@objectstack/spec';
3
- type RouteHandlerMetadata = System.RouteHandlerMetadata;
4
- type HttpMethod = Shared.HttpMethod;
5
- /**
6
- * Route Entry
7
- * Internal representation of registered routes
8
- */
9
- export interface RouteEntry {
10
- method: HttpMethod;
11
- path: string;
12
- handler: RouteHandler;
13
- metadata?: RouteHandlerMetadata['metadata'];
14
- security?: RouteHandlerMetadata['security'];
15
- }
16
- /**
17
- * RouteManager
18
- *
19
- * Manages route registration and organization for HTTP servers.
20
- * Provides:
21
- * - Route registration with metadata
22
- * - Route lookup and querying
23
- * - Bulk route registration
24
- * - Route grouping by prefix
25
- *
26
- * @example
27
- * const manager = new RouteManager(server);
28
- *
29
- * // Register individual route
30
- * manager.register({
31
- * method: 'GET',
32
- * path: '/api/users/:id',
33
- * handler: getUserHandler,
34
- * metadata: {
35
- * summary: 'Get user by ID',
36
- * tags: ['users']
37
- * }
38
- * });
39
- *
40
- * // Register route group
41
- * manager.group('/api/users', (group) => {
42
- * group.get('/', listUsersHandler);
43
- * group.post('/', createUserHandler);
44
- * group.get('/:id', getUserHandler);
45
- * });
46
- */
47
- export declare class RouteManager {
48
- private server;
49
- private routes;
50
- constructor(server: IHttpServer);
51
- /**
52
- * Register a route
53
- * @param entry - Route entry with method, path, handler, and metadata
54
- */
55
- register(entry: Omit<RouteEntry, 'handler'> & {
56
- handler: RouteHandler | string;
57
- }): void;
58
- /**
59
- * Register multiple routes
60
- * @param entries - Array of route entries
61
- */
62
- registerMany(entries: Array<Omit<RouteEntry, 'handler'> & {
63
- handler: RouteHandler | string;
64
- }>): void;
65
- /**
66
- * Unregister a route
67
- * @param method - HTTP method
68
- * @param path - Route path
69
- */
70
- unregister(method: HttpMethod, path: string): void;
71
- /**
72
- * Get route by method and path
73
- * @param method - HTTP method
74
- * @param path - Route path
75
- */
76
- get(method: HttpMethod, path: string): RouteEntry | undefined;
77
- /**
78
- * Get all routes
79
- */
80
- getAll(): RouteEntry[];
81
- /**
82
- * Get routes by method
83
- * @param method - HTTP method
84
- */
85
- getByMethod(method: HttpMethod): RouteEntry[];
86
- /**
87
- * Get routes by path prefix
88
- * @param prefix - Path prefix
89
- */
90
- getByPrefix(prefix: string): RouteEntry[];
91
- /**
92
- * Get routes by tag
93
- * @param tag - Tag name
94
- */
95
- getByTag(tag: string): RouteEntry[];
96
- /**
97
- * Create a route group with common prefix
98
- * @param prefix - Common path prefix
99
- * @param configure - Function to configure routes in the group
100
- */
101
- group(prefix: string, configure: (group: RouteGroupBuilder) => void): void;
102
- /**
103
- * Get route count
104
- */
105
- count(): number;
106
- /**
107
- * Clear all routes
108
- */
109
- clear(): void;
110
- /**
111
- * Get route key for storage
112
- */
113
- private getRouteKey;
114
- /**
115
- * Register route with underlying server
116
- */
117
- private registerWithServer;
118
- }
119
- /**
120
- * RouteGroupBuilder
121
- *
122
- * Builder for creating route groups with common prefix
123
- */
124
- export declare class RouteGroupBuilder {
125
- private manager;
126
- private prefix;
127
- constructor(manager: RouteManager, prefix: string);
128
- /**
129
- * Register GET route in group
130
- */
131
- get(path: string, handler: RouteHandler, metadata?: RouteHandlerMetadata['metadata']): this;
132
- /**
133
- * Register POST route in group
134
- */
135
- post(path: string, handler: RouteHandler, metadata?: RouteHandlerMetadata['metadata']): this;
136
- /**
137
- * Register PUT route in group
138
- */
139
- put(path: string, handler: RouteHandler, metadata?: RouteHandlerMetadata['metadata']): this;
140
- /**
141
- * Register PATCH route in group
142
- */
143
- patch(path: string, handler: RouteHandler, metadata?: RouteHandlerMetadata['metadata']): this;
144
- /**
145
- * Register DELETE route in group
146
- */
147
- delete(path: string, handler: RouteHandler, metadata?: RouteHandlerMetadata['metadata']): this;
148
- /**
149
- * Resolve full path with prefix
150
- */
151
- private resolvePath;
152
- }
153
- export {};