@objectstack/runtime 0.6.1 → 0.7.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.
@@ -0,0 +1,74 @@
1
+ import { IHttpServer } from '@objectstack/core';
2
+ import { RouteManager } from './route-manager';
3
+ import { RestServerConfig } from '@objectstack/spec/api';
4
+ import { ObjectStackProtocol } from '@objectstack/spec/api';
5
+ /**
6
+ * RestServer
7
+ *
8
+ * Provides automatic REST API endpoint generation for ObjectStack.
9
+ * Generates standard RESTful CRUD endpoints, metadata endpoints, and batch operations
10
+ * based on the configured protocol provider.
11
+ *
12
+ * Features:
13
+ * - Automatic CRUD endpoint generation (GET, POST, PUT, PATCH, DELETE)
14
+ * - Metadata API endpoints (/meta)
15
+ * - Batch operation endpoints (/batch, /createMany, /updateMany, /deleteMany)
16
+ * - Discovery endpoint
17
+ * - Configurable path prefixes and patterns
18
+ *
19
+ * @example
20
+ * const restServer = new RestServer(httpServer, protocolProvider, {
21
+ * api: {
22
+ * version: 'v1',
23
+ * basePath: '/api'
24
+ * },
25
+ * crud: {
26
+ * dataPrefix: '/data'
27
+ * }
28
+ * });
29
+ *
30
+ * restServer.registerRoutes();
31
+ */
32
+ export declare class RestServer {
33
+ private server;
34
+ private protocol;
35
+ private config;
36
+ private routeManager;
37
+ constructor(server: IHttpServer, protocol: ObjectStackProtocol, config?: RestServerConfig);
38
+ /**
39
+ * Normalize configuration with defaults
40
+ */
41
+ private normalizeConfig;
42
+ /**
43
+ * Get the full API base path
44
+ */
45
+ private getApiBasePath;
46
+ /**
47
+ * Register all REST API routes
48
+ */
49
+ registerRoutes(): void;
50
+ /**
51
+ * Register discovery endpoints
52
+ */
53
+ private registerDiscoveryEndpoints;
54
+ /**
55
+ * Register metadata endpoints
56
+ */
57
+ private registerMetadataEndpoints;
58
+ /**
59
+ * Register CRUD endpoints for data operations
60
+ */
61
+ private registerCrudEndpoints;
62
+ /**
63
+ * Register batch operation endpoints
64
+ */
65
+ private registerBatchEndpoints;
66
+ /**
67
+ * Get the route manager
68
+ */
69
+ getRouteManager(): RouteManager;
70
+ /**
71
+ * Get all registered routes
72
+ */
73
+ getRoutes(): import("./route-manager").RouteEntry[];
74
+ }
@@ -0,0 +1,490 @@
1
+ import { RouteManager } from './route-manager';
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
+ }
254
+ /**
255
+ * Register CRUD endpoints for data operations
256
+ */
257
+ registerCrudEndpoints(basePath) {
258
+ const { crud } = this.config;
259
+ const dataPath = `${basePath}${crud.dataPrefix}`;
260
+ const operations = crud.operations;
261
+ // GET /data/:object - List/query records
262
+ if (operations.list) {
263
+ this.routeManager.register({
264
+ method: 'GET',
265
+ path: `${dataPath}/:object`,
266
+ handler: async (req, res) => {
267
+ try {
268
+ const result = await this.protocol.findData({
269
+ object: req.params.object,
270
+ query: req.query
271
+ });
272
+ res.json(result);
273
+ }
274
+ catch (error) {
275
+ res.status(400).json({ error: error.message });
276
+ }
277
+ },
278
+ metadata: {
279
+ summary: 'Query records',
280
+ tags: ['data', 'crud'],
281
+ },
282
+ });
283
+ }
284
+ // GET /data/:object/:id - Get single record
285
+ if (operations.read) {
286
+ this.routeManager.register({
287
+ method: 'GET',
288
+ path: `${dataPath}/:object/:id`,
289
+ handler: async (req, res) => {
290
+ try {
291
+ const result = await this.protocol.getData({
292
+ object: req.params.object,
293
+ id: req.params.id
294
+ });
295
+ res.json(result);
296
+ }
297
+ catch (error) {
298
+ res.status(404).json({ error: error.message });
299
+ }
300
+ },
301
+ metadata: {
302
+ summary: 'Get record by ID',
303
+ tags: ['data', 'crud'],
304
+ },
305
+ });
306
+ }
307
+ // POST /data/:object - Create record
308
+ if (operations.create) {
309
+ this.routeManager.register({
310
+ method: 'POST',
311
+ path: `${dataPath}/:object`,
312
+ handler: async (req, res) => {
313
+ try {
314
+ const result = await this.protocol.createData({
315
+ object: req.params.object,
316
+ data: req.body
317
+ });
318
+ res.status(201).json(result);
319
+ }
320
+ catch (error) {
321
+ res.status(400).json({ error: error.message });
322
+ }
323
+ },
324
+ metadata: {
325
+ summary: 'Create record',
326
+ tags: ['data', 'crud'],
327
+ },
328
+ });
329
+ }
330
+ // PATCH /data/:object/:id - Update record
331
+ if (operations.update) {
332
+ this.routeManager.register({
333
+ method: 'PATCH',
334
+ path: `${dataPath}/:object/:id`,
335
+ handler: async (req, res) => {
336
+ try {
337
+ const result = await this.protocol.updateData({
338
+ object: req.params.object,
339
+ id: req.params.id,
340
+ data: req.body
341
+ });
342
+ res.json(result);
343
+ }
344
+ catch (error) {
345
+ res.status(400).json({ error: error.message });
346
+ }
347
+ },
348
+ metadata: {
349
+ summary: 'Update record',
350
+ tags: ['data', 'crud'],
351
+ },
352
+ });
353
+ }
354
+ // DELETE /data/:object/:id - Delete record
355
+ if (operations.delete) {
356
+ this.routeManager.register({
357
+ method: 'DELETE',
358
+ path: `${dataPath}/:object/:id`,
359
+ handler: async (req, res) => {
360
+ try {
361
+ const result = await this.protocol.deleteData({
362
+ object: req.params.object,
363
+ id: req.params.id
364
+ });
365
+ res.json(result);
366
+ }
367
+ catch (error) {
368
+ res.status(400).json({ error: error.message });
369
+ }
370
+ },
371
+ metadata: {
372
+ summary: 'Delete record',
373
+ tags: ['data', 'crud'],
374
+ },
375
+ });
376
+ }
377
+ }
378
+ /**
379
+ * Register batch operation endpoints
380
+ */
381
+ registerBatchEndpoints(basePath) {
382
+ const { crud, batch } = this.config;
383
+ const dataPath = `${basePath}${crud.dataPrefix}`;
384
+ const operations = batch.operations;
385
+ // POST /data/:object/batch - Generic batch endpoint
386
+ if (batch.enableBatchEndpoint && this.protocol.batchData) {
387
+ this.routeManager.register({
388
+ method: 'POST',
389
+ path: `${dataPath}/:object/batch`,
390
+ handler: async (req, res) => {
391
+ try {
392
+ const result = await this.protocol.batchData({
393
+ object: req.params.object,
394
+ request: req.body
395
+ });
396
+ res.json(result);
397
+ }
398
+ catch (error) {
399
+ res.status(400).json({ error: error.message });
400
+ }
401
+ },
402
+ metadata: {
403
+ summary: 'Batch operations',
404
+ tags: ['data', 'batch'],
405
+ },
406
+ });
407
+ }
408
+ // POST /data/:object/createMany - Bulk create
409
+ if (operations.createMany && this.protocol.createManyData) {
410
+ this.routeManager.register({
411
+ method: 'POST',
412
+ path: `${dataPath}/:object/createMany`,
413
+ handler: async (req, res) => {
414
+ try {
415
+ const result = await this.protocol.createManyData({
416
+ object: req.params.object,
417
+ records: req.body || []
418
+ });
419
+ res.status(201).json(result);
420
+ }
421
+ catch (error) {
422
+ res.status(400).json({ error: error.message });
423
+ }
424
+ },
425
+ metadata: {
426
+ summary: 'Create multiple records',
427
+ tags: ['data', 'batch'],
428
+ },
429
+ });
430
+ }
431
+ // POST /data/:object/updateMany - Bulk update
432
+ if (operations.updateMany && this.protocol.updateManyData) {
433
+ this.routeManager.register({
434
+ method: 'POST',
435
+ path: `${dataPath}/:object/updateMany`,
436
+ handler: async (req, res) => {
437
+ try {
438
+ const result = await this.protocol.updateManyData({
439
+ object: req.params.object,
440
+ ...req.body
441
+ });
442
+ res.json(result);
443
+ }
444
+ catch (error) {
445
+ res.status(400).json({ error: error.message });
446
+ }
447
+ },
448
+ metadata: {
449
+ summary: 'Update multiple records',
450
+ tags: ['data', 'batch'],
451
+ },
452
+ });
453
+ }
454
+ // POST /data/:object/deleteMany - Bulk delete
455
+ if (operations.deleteMany && this.protocol.deleteManyData) {
456
+ this.routeManager.register({
457
+ method: 'POST',
458
+ path: `${dataPath}/:object/deleteMany`,
459
+ handler: async (req, res) => {
460
+ try {
461
+ const result = await this.protocol.deleteManyData({
462
+ object: req.params.object,
463
+ ...req.body
464
+ });
465
+ res.json(result);
466
+ }
467
+ catch (error) {
468
+ res.status(400).json({ error: error.message });
469
+ }
470
+ },
471
+ metadata: {
472
+ summary: 'Delete multiple records',
473
+ tags: ['data', 'batch'],
474
+ },
475
+ });
476
+ }
477
+ }
478
+ /**
479
+ * Get the route manager
480
+ */
481
+ getRouteManager() {
482
+ return this.routeManager;
483
+ }
484
+ /**
485
+ * Get all registered routes
486
+ */
487
+ getRoutes() {
488
+ return this.routeManager.getAll();
489
+ }
490
+ }