@objectstack/rest 1.1.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/dist/index.js ADDED
@@ -0,0 +1,757 @@
1
+ // src/route-manager.ts
2
+ var RouteManager = class {
3
+ constructor(server) {
4
+ this.server = server;
5
+ this.routes = /* @__PURE__ */ new Map();
6
+ }
7
+ /**
8
+ * Register a route
9
+ * @param entry - Route entry with method, path, handler, and metadata
10
+ */
11
+ register(entry) {
12
+ if (typeof entry.handler === "string") {
13
+ throw new Error(
14
+ `String-based route handlers are not supported yet. Received handler identifier "${entry.handler}". Please provide a RouteHandler function instead.`
15
+ );
16
+ }
17
+ const handler = entry.handler;
18
+ const routeEntry = {
19
+ method: entry.method,
20
+ path: entry.path,
21
+ handler,
22
+ metadata: entry.metadata,
23
+ security: entry.security
24
+ };
25
+ const key = this.getRouteKey(entry.method, entry.path);
26
+ this.routes.set(key, routeEntry);
27
+ this.registerWithServer(routeEntry);
28
+ }
29
+ /**
30
+ * Register multiple routes
31
+ * @param entries - Array of route entries
32
+ */
33
+ registerMany(entries) {
34
+ entries.forEach((entry) => this.register(entry));
35
+ }
36
+ /**
37
+ * Unregister a route
38
+ * @param method - HTTP method
39
+ * @param path - Route path
40
+ */
41
+ unregister(method, path) {
42
+ const key = this.getRouteKey(method, path);
43
+ this.routes.delete(key);
44
+ }
45
+ /**
46
+ * Get route by method and path
47
+ * @param method - HTTP method
48
+ * @param path - Route path
49
+ */
50
+ get(method, path) {
51
+ const key = this.getRouteKey(method, path);
52
+ return this.routes.get(key);
53
+ }
54
+ /**
55
+ * Get all routes
56
+ */
57
+ getAll() {
58
+ return Array.from(this.routes.values());
59
+ }
60
+ /**
61
+ * Get routes by method
62
+ * @param method - HTTP method
63
+ */
64
+ getByMethod(method) {
65
+ return this.getAll().filter((route) => route.method === method);
66
+ }
67
+ /**
68
+ * Get routes by path prefix
69
+ * @param prefix - Path prefix
70
+ */
71
+ getByPrefix(prefix) {
72
+ return this.getAll().filter((route) => route.path.startsWith(prefix));
73
+ }
74
+ /**
75
+ * Get routes by tag
76
+ * @param tag - Tag name
77
+ */
78
+ getByTag(tag) {
79
+ return this.getAll().filter(
80
+ (route) => route.metadata?.tags?.includes(tag)
81
+ );
82
+ }
83
+ /**
84
+ * Create a route group with common prefix
85
+ * @param prefix - Common path prefix
86
+ * @param configure - Function to configure routes in the group
87
+ */
88
+ group(prefix, configure) {
89
+ const builder = new RouteGroupBuilder(this, prefix);
90
+ configure(builder);
91
+ }
92
+ /**
93
+ * Get route count
94
+ */
95
+ count() {
96
+ return this.routes.size;
97
+ }
98
+ /**
99
+ * Clear all routes
100
+ */
101
+ clear() {
102
+ this.routes.clear();
103
+ }
104
+ /**
105
+ * Get route key for storage
106
+ */
107
+ getRouteKey(method, path) {
108
+ return `${method}:${path}`;
109
+ }
110
+ /**
111
+ * Register route with underlying server
112
+ */
113
+ registerWithServer(entry) {
114
+ const { method, path, handler } = entry;
115
+ switch (method) {
116
+ case "GET":
117
+ this.server.get(path, handler);
118
+ break;
119
+ case "POST":
120
+ this.server.post(path, handler);
121
+ break;
122
+ case "PUT":
123
+ this.server.put(path, handler);
124
+ break;
125
+ case "DELETE":
126
+ this.server.delete(path, handler);
127
+ break;
128
+ case "PATCH":
129
+ this.server.patch(path, handler);
130
+ break;
131
+ default:
132
+ throw new Error(`Unsupported HTTP method: ${method}`);
133
+ }
134
+ }
135
+ };
136
+ var RouteGroupBuilder = class {
137
+ constructor(manager, prefix) {
138
+ this.manager = manager;
139
+ this.prefix = prefix;
140
+ }
141
+ /**
142
+ * Register GET route in group
143
+ */
144
+ get(path, handler, metadata) {
145
+ this.manager.register({
146
+ method: "GET",
147
+ path: this.resolvePath(path),
148
+ handler,
149
+ metadata
150
+ });
151
+ return this;
152
+ }
153
+ /**
154
+ * Register POST route in group
155
+ */
156
+ post(path, handler, metadata) {
157
+ this.manager.register({
158
+ method: "POST",
159
+ path: this.resolvePath(path),
160
+ handler,
161
+ metadata
162
+ });
163
+ return this;
164
+ }
165
+ /**
166
+ * Register PUT route in group
167
+ */
168
+ put(path, handler, metadata) {
169
+ this.manager.register({
170
+ method: "PUT",
171
+ path: this.resolvePath(path),
172
+ handler,
173
+ metadata
174
+ });
175
+ return this;
176
+ }
177
+ /**
178
+ * Register PATCH route in group
179
+ */
180
+ patch(path, handler, metadata) {
181
+ this.manager.register({
182
+ method: "PATCH",
183
+ path: this.resolvePath(path),
184
+ handler,
185
+ metadata
186
+ });
187
+ return this;
188
+ }
189
+ /**
190
+ * Register DELETE route in group
191
+ */
192
+ delete(path, handler, metadata) {
193
+ this.manager.register({
194
+ method: "DELETE",
195
+ path: this.resolvePath(path),
196
+ handler,
197
+ metadata
198
+ });
199
+ return this;
200
+ }
201
+ /**
202
+ * Resolve full path with prefix
203
+ */
204
+ resolvePath(path) {
205
+ const normalizedPrefix = this.prefix.endsWith("/") ? this.prefix.slice(0, -1) : this.prefix;
206
+ const normalizedPath = path.startsWith("/") ? path : "/" + path;
207
+ return normalizedPrefix + normalizedPath;
208
+ }
209
+ };
210
+
211
+ // src/rest-server.ts
212
+ var RestServer = class {
213
+ constructor(server, protocol, config = {}) {
214
+ this.protocol = protocol;
215
+ this.config = this.normalizeConfig(config);
216
+ this.routeManager = new RouteManager(server);
217
+ }
218
+ /**
219
+ * Normalize configuration with defaults
220
+ */
221
+ normalizeConfig(config) {
222
+ const api = config.api ?? {};
223
+ const crud = config.crud ?? {};
224
+ const metadata = config.metadata ?? {};
225
+ const batch = config.batch ?? {};
226
+ const routes = config.routes ?? {};
227
+ return {
228
+ api: {
229
+ version: api.version ?? "v1",
230
+ basePath: api.basePath ?? "/api",
231
+ apiPath: api.apiPath,
232
+ enableCrud: api.enableCrud ?? true,
233
+ enableMetadata: api.enableMetadata ?? true,
234
+ enableUi: api.enableUi ?? true,
235
+ enableBatch: api.enableBatch ?? true,
236
+ enableDiscovery: api.enableDiscovery ?? true,
237
+ documentation: api.documentation,
238
+ responseFormat: api.responseFormat
239
+ },
240
+ crud: {
241
+ operations: crud.operations ?? {
242
+ create: true,
243
+ read: true,
244
+ update: true,
245
+ delete: true,
246
+ list: true
247
+ },
248
+ patterns: crud.patterns,
249
+ dataPrefix: crud.dataPrefix ?? "/data",
250
+ objectParamStyle: crud.objectParamStyle ?? "path"
251
+ },
252
+ metadata: {
253
+ prefix: metadata.prefix ?? "/meta",
254
+ enableCache: metadata.enableCache ?? true,
255
+ cacheTtl: metadata.cacheTtl ?? 3600,
256
+ endpoints: metadata.endpoints ?? {
257
+ types: true,
258
+ items: true,
259
+ item: true,
260
+ schema: true
261
+ }
262
+ },
263
+ batch: {
264
+ maxBatchSize: batch.maxBatchSize ?? 200,
265
+ enableBatchEndpoint: batch.enableBatchEndpoint ?? true,
266
+ operations: batch.operations ?? {
267
+ createMany: true,
268
+ updateMany: true,
269
+ deleteMany: true,
270
+ upsertMany: true
271
+ },
272
+ defaultAtomic: batch.defaultAtomic ?? true
273
+ },
274
+ routes: {
275
+ includeObjects: routes.includeObjects,
276
+ excludeObjects: routes.excludeObjects,
277
+ nameTransform: routes.nameTransform ?? "none",
278
+ overrides: routes.overrides
279
+ }
280
+ };
281
+ }
282
+ /**
283
+ * Get the full API base path
284
+ */
285
+ getApiBasePath() {
286
+ const { api } = this.config;
287
+ return api.apiPath ?? `${api.basePath}/${api.version}`;
288
+ }
289
+ /**
290
+ * Register all REST API routes
291
+ */
292
+ registerRoutes() {
293
+ const basePath = this.getApiBasePath();
294
+ if (this.config.api.enableDiscovery) {
295
+ this.registerDiscoveryEndpoints(basePath);
296
+ }
297
+ if (this.config.api.enableMetadata) {
298
+ this.registerMetadataEndpoints(basePath);
299
+ }
300
+ if (this.config.api.enableUi) {
301
+ this.registerUiEndpoints(basePath);
302
+ }
303
+ if (this.config.api.enableCrud) {
304
+ this.registerCrudEndpoints(basePath);
305
+ }
306
+ if (this.config.api.enableBatch) {
307
+ this.registerBatchEndpoints(basePath);
308
+ }
309
+ }
310
+ /**
311
+ * Register discovery endpoints
312
+ */
313
+ registerDiscoveryEndpoints(basePath) {
314
+ this.routeManager.register({
315
+ method: "GET",
316
+ path: basePath,
317
+ handler: async (_req, res) => {
318
+ try {
319
+ const discovery = await this.protocol.getDiscovery();
320
+ discovery.version = this.config.api.version;
321
+ if (discovery.endpoints) {
322
+ if (this.config.api.enableCrud) {
323
+ discovery.endpoints.data = `${basePath}${this.config.crud.dataPrefix}`;
324
+ }
325
+ if (this.config.api.enableMetadata) {
326
+ discovery.endpoints.metadata = `${basePath}${this.config.metadata.prefix}`;
327
+ }
328
+ if (this.config.api.enableUi) {
329
+ discovery.endpoints.ui = `${basePath}/ui`;
330
+ }
331
+ if (discovery.endpoints.auth) {
332
+ discovery.endpoints.auth = `${basePath}/auth`;
333
+ }
334
+ }
335
+ res.json(discovery);
336
+ } catch (error) {
337
+ res.status(500).json({ error: error.message });
338
+ }
339
+ },
340
+ metadata: {
341
+ summary: "Get API discovery information",
342
+ tags: ["discovery"]
343
+ }
344
+ });
345
+ }
346
+ /**
347
+ * Register metadata endpoints
348
+ */
349
+ registerMetadataEndpoints(basePath) {
350
+ const { metadata } = this.config;
351
+ const metaPath = `${basePath}${metadata.prefix}`;
352
+ if (metadata.endpoints.types !== false) {
353
+ this.routeManager.register({
354
+ method: "GET",
355
+ path: metaPath,
356
+ handler: async (_req, res) => {
357
+ try {
358
+ const types = await this.protocol.getMetaTypes();
359
+ res.json(types);
360
+ } catch (error) {
361
+ res.status(500).json({ error: error.message });
362
+ }
363
+ },
364
+ metadata: {
365
+ summary: "List all metadata types",
366
+ tags: ["metadata"]
367
+ }
368
+ });
369
+ }
370
+ if (metadata.endpoints.items !== false) {
371
+ this.routeManager.register({
372
+ method: "GET",
373
+ path: `${metaPath}/:type`,
374
+ handler: async (req, res) => {
375
+ try {
376
+ const items = await this.protocol.getMetaItems({ type: req.params.type });
377
+ res.json(items);
378
+ } catch (error) {
379
+ res.status(404).json({ error: error.message });
380
+ }
381
+ },
382
+ metadata: {
383
+ summary: "List metadata items of a type",
384
+ tags: ["metadata"]
385
+ }
386
+ });
387
+ }
388
+ if (metadata.endpoints.item !== false) {
389
+ this.routeManager.register({
390
+ method: "GET",
391
+ path: `${metaPath}/:type/:name`,
392
+ handler: async (req, res) => {
393
+ try {
394
+ if (metadata.enableCache && this.protocol.getMetaItemCached) {
395
+ const cacheRequest = {
396
+ ifNoneMatch: req.headers["if-none-match"],
397
+ ifModifiedSince: req.headers["if-modified-since"]
398
+ };
399
+ const result = await this.protocol.getMetaItemCached({
400
+ type: req.params.type,
401
+ name: req.params.name,
402
+ cacheRequest
403
+ });
404
+ if (result.notModified) {
405
+ res.status(304).send();
406
+ return;
407
+ }
408
+ if (result.etag) {
409
+ const etagValue = result.etag.weak ? `W/"${result.etag.value}"` : `"${result.etag.value}"`;
410
+ res.header("ETag", etagValue);
411
+ }
412
+ if (result.lastModified) {
413
+ res.header("Last-Modified", new Date(result.lastModified).toUTCString());
414
+ }
415
+ if (result.cacheControl) {
416
+ const directives = result.cacheControl.directives.join(", ");
417
+ const maxAge = result.cacheControl.maxAge ? `, max-age=${result.cacheControl.maxAge}` : "";
418
+ res.header("Cache-Control", directives + maxAge);
419
+ }
420
+ res.json(result.data);
421
+ } else {
422
+ const item = await this.protocol.getMetaItem({ type: req.params.type, name: req.params.name });
423
+ res.json(item);
424
+ }
425
+ } catch (error) {
426
+ res.status(404).json({ error: error.message });
427
+ }
428
+ },
429
+ metadata: {
430
+ summary: "Get specific metadata item",
431
+ tags: ["metadata"]
432
+ }
433
+ });
434
+ }
435
+ this.routeManager.register({
436
+ method: "PUT",
437
+ path: `${metaPath}/:type/:name`,
438
+ handler: async (req, res) => {
439
+ try {
440
+ if (!this.protocol.saveMetaItem) {
441
+ res.status(501).json({ error: "Save operation not supported by protocol implementation" });
442
+ return;
443
+ }
444
+ const result = await this.protocol.saveMetaItem({
445
+ type: req.params.type,
446
+ name: req.params.name,
447
+ item: req.body
448
+ });
449
+ res.json(result);
450
+ } catch (error) {
451
+ res.status(400).json({ error: error.message });
452
+ }
453
+ },
454
+ metadata: {
455
+ summary: "Save specific metadata item",
456
+ tags: ["metadata"]
457
+ }
458
+ });
459
+ }
460
+ /**
461
+ * Register UI endpoints
462
+ */
463
+ registerUiEndpoints(basePath) {
464
+ const uiPath = `${basePath}/ui`;
465
+ this.routeManager.register({
466
+ method: "GET",
467
+ path: `${uiPath}/view/:object/:type`,
468
+ handler: async (req, res) => {
469
+ try {
470
+ if (this.protocol.getUiView) {
471
+ const view = await this.protocol.getUiView({
472
+ object: req.params.object,
473
+ type: req.params.type
474
+ });
475
+ res.json(view);
476
+ } else {
477
+ res.status(501).json({ error: "UI View resolution not supported by protocol implementation" });
478
+ }
479
+ } catch (error) {
480
+ res.status(404).json({ error: error.message });
481
+ }
482
+ },
483
+ metadata: {
484
+ summary: "Resolve UI View for object",
485
+ tags: ["ui"]
486
+ }
487
+ });
488
+ }
489
+ /**
490
+ * Register CRUD endpoints for data operations
491
+ */
492
+ registerCrudEndpoints(basePath) {
493
+ const { crud } = this.config;
494
+ const dataPath = `${basePath}${crud.dataPrefix}`;
495
+ const operations = crud.operations;
496
+ if (operations.list) {
497
+ this.routeManager.register({
498
+ method: "GET",
499
+ path: `${dataPath}/:object`,
500
+ handler: async (req, res) => {
501
+ try {
502
+ const result = await this.protocol.findData({
503
+ object: req.params.object,
504
+ query: req.query
505
+ });
506
+ res.json(result);
507
+ } catch (error) {
508
+ res.status(400).json({ error: error.message });
509
+ }
510
+ },
511
+ metadata: {
512
+ summary: "Query records",
513
+ tags: ["data", "crud"]
514
+ }
515
+ });
516
+ }
517
+ if (operations.read) {
518
+ this.routeManager.register({
519
+ method: "GET",
520
+ path: `${dataPath}/:object/:id`,
521
+ handler: async (req, res) => {
522
+ try {
523
+ const result = await this.protocol.getData({
524
+ object: req.params.object,
525
+ id: req.params.id
526
+ });
527
+ res.json(result);
528
+ } catch (error) {
529
+ res.status(404).json({ error: error.message });
530
+ }
531
+ },
532
+ metadata: {
533
+ summary: "Get record by ID",
534
+ tags: ["data", "crud"]
535
+ }
536
+ });
537
+ }
538
+ if (operations.create) {
539
+ this.routeManager.register({
540
+ method: "POST",
541
+ path: `${dataPath}/:object`,
542
+ handler: async (req, res) => {
543
+ try {
544
+ const result = await this.protocol.createData({
545
+ object: req.params.object,
546
+ data: req.body
547
+ });
548
+ res.status(201).json(result);
549
+ } catch (error) {
550
+ res.status(400).json({ error: error.message });
551
+ }
552
+ },
553
+ metadata: {
554
+ summary: "Create record",
555
+ tags: ["data", "crud"]
556
+ }
557
+ });
558
+ }
559
+ if (operations.update) {
560
+ this.routeManager.register({
561
+ method: "PATCH",
562
+ path: `${dataPath}/:object/:id`,
563
+ handler: async (req, res) => {
564
+ try {
565
+ const result = await this.protocol.updateData({
566
+ object: req.params.object,
567
+ id: req.params.id,
568
+ data: req.body
569
+ });
570
+ res.json(result);
571
+ } catch (error) {
572
+ res.status(400).json({ error: error.message });
573
+ }
574
+ },
575
+ metadata: {
576
+ summary: "Update record",
577
+ tags: ["data", "crud"]
578
+ }
579
+ });
580
+ }
581
+ if (operations.delete) {
582
+ this.routeManager.register({
583
+ method: "DELETE",
584
+ path: `${dataPath}/:object/:id`,
585
+ handler: async (req, res) => {
586
+ try {
587
+ const result = await this.protocol.deleteData({
588
+ object: req.params.object,
589
+ id: req.params.id
590
+ });
591
+ res.json(result);
592
+ } catch (error) {
593
+ res.status(400).json({ error: error.message });
594
+ }
595
+ },
596
+ metadata: {
597
+ summary: "Delete record",
598
+ tags: ["data", "crud"]
599
+ }
600
+ });
601
+ }
602
+ }
603
+ /**
604
+ * Register batch operation endpoints
605
+ */
606
+ registerBatchEndpoints(basePath) {
607
+ const { crud, batch } = this.config;
608
+ const dataPath = `${basePath}${crud.dataPrefix}`;
609
+ const operations = batch.operations;
610
+ if (batch.enableBatchEndpoint && this.protocol.batchData) {
611
+ this.routeManager.register({
612
+ method: "POST",
613
+ path: `${dataPath}/:object/batch`,
614
+ handler: async (req, res) => {
615
+ try {
616
+ const result = await this.protocol.batchData({
617
+ object: req.params.object,
618
+ request: req.body
619
+ });
620
+ res.json(result);
621
+ } catch (error) {
622
+ res.status(400).json({ error: error.message });
623
+ }
624
+ },
625
+ metadata: {
626
+ summary: "Batch operations",
627
+ tags: ["data", "batch"]
628
+ }
629
+ });
630
+ }
631
+ if (operations.createMany && this.protocol.createManyData) {
632
+ this.routeManager.register({
633
+ method: "POST",
634
+ path: `${dataPath}/:object/createMany`,
635
+ handler: async (req, res) => {
636
+ try {
637
+ const result = await this.protocol.createManyData({
638
+ object: req.params.object,
639
+ records: req.body || []
640
+ });
641
+ res.status(201).json(result);
642
+ } catch (error) {
643
+ res.status(400).json({ error: error.message });
644
+ }
645
+ },
646
+ metadata: {
647
+ summary: "Create multiple records",
648
+ tags: ["data", "batch"]
649
+ }
650
+ });
651
+ }
652
+ if (operations.updateMany && this.protocol.updateManyData) {
653
+ this.routeManager.register({
654
+ method: "POST",
655
+ path: `${dataPath}/:object/updateMany`,
656
+ handler: async (req, res) => {
657
+ try {
658
+ const result = await this.protocol.updateManyData({
659
+ object: req.params.object,
660
+ ...req.body
661
+ });
662
+ res.json(result);
663
+ } catch (error) {
664
+ res.status(400).json({ error: error.message });
665
+ }
666
+ },
667
+ metadata: {
668
+ summary: "Update multiple records",
669
+ tags: ["data", "batch"]
670
+ }
671
+ });
672
+ }
673
+ if (operations.deleteMany && this.protocol.deleteManyData) {
674
+ this.routeManager.register({
675
+ method: "POST",
676
+ path: `${dataPath}/:object/deleteMany`,
677
+ handler: async (req, res) => {
678
+ try {
679
+ const result = await this.protocol.deleteManyData({
680
+ object: req.params.object,
681
+ ...req.body
682
+ });
683
+ res.json(result);
684
+ } catch (error) {
685
+ res.status(400).json({ error: error.message });
686
+ }
687
+ },
688
+ metadata: {
689
+ summary: "Delete multiple records",
690
+ tags: ["data", "batch"]
691
+ }
692
+ });
693
+ }
694
+ }
695
+ /**
696
+ * Get the route manager
697
+ */
698
+ getRouteManager() {
699
+ return this.routeManager;
700
+ }
701
+ /**
702
+ * Get all registered routes
703
+ */
704
+ getRoutes() {
705
+ return this.routeManager.getAll();
706
+ }
707
+ };
708
+
709
+ // src/rest-api-plugin.ts
710
+ function createRestApiPlugin(config = {}) {
711
+ return {
712
+ name: "com.objectstack.rest.api",
713
+ version: "1.0.0",
714
+ init: async (_ctx) => {
715
+ },
716
+ start: async (ctx) => {
717
+ const serverService = config.serverServiceName || "http.server";
718
+ const protocolService = config.protocolServiceName || "protocol";
719
+ let server;
720
+ let protocol;
721
+ try {
722
+ server = ctx.getService(serverService);
723
+ } catch (e) {
724
+ }
725
+ try {
726
+ protocol = ctx.getService(protocolService);
727
+ } catch (e) {
728
+ }
729
+ if (!server) {
730
+ ctx.logger.warn(`RestApiPlugin: HTTP Server service '${serverService}' not found. REST routes skipped.`);
731
+ return;
732
+ }
733
+ if (!protocol) {
734
+ ctx.logger.warn(`RestApiPlugin: Protocol service '${protocolService}' not found. REST routes skipped.`);
735
+ return;
736
+ }
737
+ ctx.logger.info("Hydrating REST API from Protocol...");
738
+ try {
739
+ const restServer = new RestServer(server, protocol, config.api);
740
+ restServer.registerRoutes();
741
+ ctx.logger.info("REST API successfully registered");
742
+ } catch (err) {
743
+ ctx.logger.error("Failed to register REST API routes", { error: err.message });
744
+ throw err;
745
+ }
746
+ }
747
+ };
748
+ }
749
+ var createApiRegistryPlugin = createRestApiPlugin;
750
+ export {
751
+ RestServer,
752
+ RouteGroupBuilder,
753
+ RouteManager,
754
+ createApiRegistryPlugin,
755
+ createRestApiPlugin
756
+ };
757
+ //# sourceMappingURL=index.js.map