@globaltypesystem/gts-ts 0.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.
Files changed (81) hide show
  1. package/.eslintrc.json +16 -0
  2. package/.github/workflows/ci.yml +198 -0
  3. package/.gitmodules +3 -0
  4. package/.prettierrc +7 -0
  5. package/LICENSE +201 -0
  6. package/Makefile +64 -0
  7. package/README.md +298 -0
  8. package/dist/cast.d.ts +9 -0
  9. package/dist/cast.d.ts.map +1 -0
  10. package/dist/cast.js +153 -0
  11. package/dist/cast.js.map +1 -0
  12. package/dist/cli/index.d.ts +3 -0
  13. package/dist/cli/index.d.ts.map +1 -0
  14. package/dist/cli/index.js +318 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/compatibility.d.ts +11 -0
  17. package/dist/compatibility.d.ts.map +1 -0
  18. package/dist/compatibility.js +176 -0
  19. package/dist/compatibility.js.map +1 -0
  20. package/dist/extract.d.ts +13 -0
  21. package/dist/extract.d.ts.map +1 -0
  22. package/dist/extract.js +194 -0
  23. package/dist/extract.js.map +1 -0
  24. package/dist/gts.d.ts +18 -0
  25. package/dist/gts.d.ts.map +1 -0
  26. package/dist/gts.js +472 -0
  27. package/dist/gts.js.map +1 -0
  28. package/dist/index.d.ts +29 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +97 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/query.d.ts +10 -0
  33. package/dist/query.d.ts.map +1 -0
  34. package/dist/query.js +171 -0
  35. package/dist/query.js.map +1 -0
  36. package/dist/relationships.d.ts +7 -0
  37. package/dist/relationships.d.ts.map +1 -0
  38. package/dist/relationships.js +80 -0
  39. package/dist/relationships.js.map +1 -0
  40. package/dist/server/index.d.ts +2 -0
  41. package/dist/server/index.d.ts.map +1 -0
  42. package/dist/server/index.js +132 -0
  43. package/dist/server/index.js.map +1 -0
  44. package/dist/server/server.d.ts +33 -0
  45. package/dist/server/server.d.ts.map +1 -0
  46. package/dist/server/server.js +678 -0
  47. package/dist/server/server.js.map +1 -0
  48. package/dist/server/types.d.ts +61 -0
  49. package/dist/server/types.d.ts.map +1 -0
  50. package/dist/server/types.js +3 -0
  51. package/dist/server/types.js.map +1 -0
  52. package/dist/store.d.ts +39 -0
  53. package/dist/store.d.ts.map +1 -0
  54. package/dist/store.js +1026 -0
  55. package/dist/store.js.map +1 -0
  56. package/dist/types.d.ts +111 -0
  57. package/dist/types.d.ts.map +1 -0
  58. package/dist/types.js +29 -0
  59. package/dist/types.js.map +1 -0
  60. package/dist/x-gts-ref.d.ts +35 -0
  61. package/dist/x-gts-ref.d.ts.map +1 -0
  62. package/dist/x-gts-ref.js +304 -0
  63. package/dist/x-gts-ref.js.map +1 -0
  64. package/jest.config.js +13 -0
  65. package/package.json +54 -0
  66. package/src/cast.ts +179 -0
  67. package/src/cli/index.ts +315 -0
  68. package/src/compatibility.ts +201 -0
  69. package/src/extract.ts +213 -0
  70. package/src/gts.ts +550 -0
  71. package/src/index.ts +97 -0
  72. package/src/query.ts +191 -0
  73. package/src/relationships.ts +91 -0
  74. package/src/server/index.ts +112 -0
  75. package/src/server/server.ts +771 -0
  76. package/src/server/types.ts +74 -0
  77. package/src/store.ts +1178 -0
  78. package/src/types.ts +138 -0
  79. package/src/x-gts-ref.ts +349 -0
  80. package/tests/gts.test.ts +525 -0
  81. package/tsconfig.json +32 -0
@@ -0,0 +1,771 @@
1
+ import Fastify, { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2
+ import { GTS, createJsonEntity } from '../index';
3
+ import { XGtsRefValidator } from '../x-gts-ref';
4
+ import {
5
+ ServerConfig,
6
+ EntityResponse,
7
+ OperationResult,
8
+ ListResult,
9
+ ValidateIDParams,
10
+ MatchPatternParams,
11
+ ValidateInstanceBody,
12
+ ResolveRelationshipsParams,
13
+ CompatibilityParams,
14
+ CastBody,
15
+ QueryParams,
16
+ } from './types';
17
+ import * as gts from '../index';
18
+
19
+ export class GtsServer {
20
+ private fastify: FastifyInstance;
21
+ private store: GTS;
22
+ private config: ServerConfig;
23
+
24
+ constructor(config: ServerConfig) {
25
+ this.config = config;
26
+ this.store = new GTS({ validateRefs: false });
27
+
28
+ this.fastify = Fastify({
29
+ logger:
30
+ config.verbose > 0
31
+ ? {
32
+ level: config.verbose >= 2 ? 'debug' : 'info',
33
+ }
34
+ : false,
35
+ });
36
+
37
+ this.setupMiddleware();
38
+ this.registerRoutes();
39
+ }
40
+
41
+ private setupMiddleware(): void {
42
+ // Enable CORS manually
43
+ this.fastify.addHook('onRequest', async (_request, reply) => {
44
+ reply.header('Access-Control-Allow-Origin', '*');
45
+ reply.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
46
+ reply.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
47
+ });
48
+
49
+ // Handle OPTIONS requests
50
+ this.fastify.options('*', async (_request, reply) => {
51
+ reply.status(204).send();
52
+ });
53
+ }
54
+
55
+ private registerRoutes(): void {
56
+ // Health check
57
+ this.fastify.get('/health', async () => ({
58
+ status: 'ok',
59
+ timestamp: new Date().toISOString(),
60
+ }));
61
+
62
+ // Entity management
63
+ this.fastify.get('/entities', this.handleGetEntities.bind(this));
64
+ this.fastify.get('/entities/:id', this.handleGetEntity.bind(this));
65
+ this.fastify.post('/entities', this.handleAddEntity.bind(this));
66
+ this.fastify.post('/entities/bulk', this.handleAddEntities.bind(this));
67
+ this.fastify.post('/schemas', this.handleAddSchema.bind(this));
68
+
69
+ // OP#1 - Validate ID
70
+ this.fastify.get('/validate-id', this.handleValidateID.bind(this));
71
+
72
+ // OP#2 - Extract ID
73
+ this.fastify.post('/extract-id', this.handleExtractID.bind(this));
74
+
75
+ // OP#3 - Parse ID
76
+ this.fastify.get('/parse-id', this.handleParseID.bind(this));
77
+
78
+ // OP#4 - Match ID Pattern
79
+ this.fastify.get('/match-id-pattern', this.handleMatchIDPattern.bind(this));
80
+
81
+ // OP#5 - UUID
82
+ this.fastify.get('/uuid', this.handleUUID.bind(this));
83
+
84
+ // OP#6 - Validate Instance
85
+ this.fastify.post('/validate-instance', this.handleValidateInstance.bind(this));
86
+
87
+ // OP#7 - Resolve Relationships
88
+ this.fastify.get('/resolve-relationships', this.handleResolveRelationships.bind(this));
89
+
90
+ // OP#8 - Compatibility
91
+ this.fastify.get('/compatibility', this.handleCompatibility.bind(this));
92
+
93
+ // OP#9 - Cast
94
+ this.fastify.post('/cast', this.handleCast.bind(this));
95
+
96
+ // OP#10 - Query
97
+ this.fastify.get('/query', this.handleQuery.bind(this));
98
+
99
+ // OP#11 - Attribute Access
100
+ this.fastify.get('/attr', this.handleAttribute.bind(this));
101
+
102
+ // OpenAPI spec
103
+ this.fastify.get('/openapi', this.handleOpenAPI.bind(this));
104
+ }
105
+
106
+ // Entity Management Handlers
107
+ private async handleGetEntities(
108
+ request: FastifyRequest<{ Querystring: { limit?: string } }>,
109
+ _reply: FastifyReply
110
+ ): Promise<ListResult> {
111
+ let limit = parseInt(request.query.limit || '100', 10);
112
+ if (limit < 1) limit = 1;
113
+ if (limit > 1000) limit = 1000;
114
+
115
+ const items = this.store['store']
116
+ .getAll()
117
+ .slice(0, limit)
118
+ .map((e) => e.id);
119
+
120
+ return {
121
+ count: items.length,
122
+ items,
123
+ };
124
+ }
125
+
126
+ private async handleGetEntity(
127
+ request: FastifyRequest<{ Params: { id: string } }>,
128
+ reply: FastifyReply
129
+ ): Promise<EntityResponse> {
130
+ const entity = this.store.get(request.params.id);
131
+
132
+ if (!entity) {
133
+ reply.code(404);
134
+ throw new Error(`Entity not found: ${request.params.id}`);
135
+ }
136
+
137
+ return {
138
+ id: request.params.id,
139
+ content: entity,
140
+ };
141
+ }
142
+
143
+ private async handleAddEntity(
144
+ request: FastifyRequest<{
145
+ Body: any;
146
+ Querystring: { validate?: string; validation?: string };
147
+ }>,
148
+ reply: FastifyReply
149
+ ): Promise<OperationResult> {
150
+ try {
151
+ const content = request.body;
152
+ const validate = request.query.validate === 'true' || request.query.validation === 'true';
153
+ const entity = createJsonEntity(content);
154
+
155
+ // Strict validation for schemas when validate=true
156
+ if (validate && entity.isSchema) {
157
+ const validationError = this.validateSchemaStrict(content);
158
+ if (validationError) {
159
+ reply.code(422);
160
+ return {
161
+ ok: false,
162
+ error: validationError,
163
+ };
164
+ }
165
+ }
166
+
167
+ // Strict validation for instances when validate=true
168
+ if (validate && !entity.isSchema) {
169
+ // Check if entity has recognizable GTS fields
170
+ if (!entity.id || !gts.isValidGtsID(entity.id)) {
171
+ // Check if there's a valid type field
172
+ const hasValidType = entity.schemaId && gts.isValidGtsID(entity.schemaId);
173
+ if (!hasValidType) {
174
+ reply.code(422);
175
+ return {
176
+ ok: false,
177
+ error: 'Instance must have a valid GTS ID or type field',
178
+ };
179
+ }
180
+ }
181
+ }
182
+
183
+ if (!entity.id) {
184
+ return {
185
+ ok: false,
186
+ error: 'Unable to extract GTS ID from entity',
187
+ };
188
+ }
189
+
190
+ // Validate schema with x-gts-ref if it's a schema
191
+ // x-gts-ref validation always returns 422 on failure (not just when validate=true)
192
+ if (entity.isSchema) {
193
+ const xGtsRefValidator = new XGtsRefValidator(this.store['store']);
194
+ const xGtsRefErrors = xGtsRefValidator.validateSchema(content);
195
+ if (xGtsRefErrors.length > 0) {
196
+ const errorMsgs = xGtsRefErrors.map((err) => `${err.fieldPath}: ${err.reason}`).join('; ');
197
+ reply.code(422);
198
+ return {
199
+ ok: false,
200
+ error: `x-gts-ref validation failed: ${errorMsgs}`,
201
+ };
202
+ }
203
+ }
204
+
205
+ // Register the entity
206
+ this.store.register(content);
207
+
208
+ // Validate instance if requested
209
+ if (validate && !entity.isSchema) {
210
+ const result = this.store.validateInstance(entity.id);
211
+ if (!result.ok) {
212
+ return {
213
+ ok: false,
214
+ error: result.error,
215
+ };
216
+ }
217
+ }
218
+
219
+ return {
220
+ ok: true,
221
+ id: entity.id,
222
+ };
223
+ } catch (error) {
224
+ return {
225
+ ok: false,
226
+ error: error instanceof Error ? error.message : String(error),
227
+ };
228
+ }
229
+ }
230
+
231
+ private validateSchemaStrict(content: any): string | null {
232
+ // Check for $id or $$id
233
+ const schemaId = content['$id'] || content['$$id'];
234
+ if (!schemaId) {
235
+ return 'Schema must have a $id or $$id field';
236
+ }
237
+
238
+ // Normalize the ID
239
+ let normalizedId = schemaId;
240
+ if (typeof normalizedId === 'string') {
241
+ // Check if it starts with gts:// prefix
242
+ if (normalizedId.startsWith('gts://')) {
243
+ normalizedId = normalizedId.substring(6);
244
+ } else if (normalizedId.startsWith('gts.')) {
245
+ // Plain gts. prefix without gts:// is not allowed for JSON Schema $id
246
+ return 'Schema $id with GTS identifier must use gts:// URI format (e.g., gts://gts.vendor.pkg.ns.type.v1~)';
247
+ } else {
248
+ // Non-GTS $id is not allowed
249
+ return 'Schema $id must be a valid GTS identifier with gts:// URI format';
250
+ }
251
+
252
+ // Check for wildcards in schema ID
253
+ if (normalizedId.includes('*')) {
254
+ return 'Schema $id cannot contain wildcards';
255
+ }
256
+
257
+ // Validate the GTS ID
258
+ if (!gts.isValidGtsID(normalizedId)) {
259
+ return `Schema $id is not a valid GTS identifier: ${normalizedId}`;
260
+ }
261
+ }
262
+
263
+ // Validate $ref fields
264
+ const refErrors = this.validateSchemaRefs(content, '');
265
+ if (refErrors.length > 0) {
266
+ return refErrors[0];
267
+ }
268
+
269
+ return null;
270
+ }
271
+
272
+ private validateSchemaRefs(obj: any, path: string): string[] {
273
+ const errors: string[] = [];
274
+
275
+ if (!obj || typeof obj !== 'object') {
276
+ return errors;
277
+ }
278
+
279
+ // Check $ref or $$ref
280
+ const ref = obj['$ref'] || obj['$$ref'];
281
+ if (typeof ref === 'string') {
282
+ const refPath = path ? `${path}/$ref` : '$ref';
283
+
284
+ // Local refs (#/...) are allowed
285
+ if (ref.startsWith('#')) {
286
+ // OK - local ref
287
+ } else if (ref.startsWith('gts://')) {
288
+ // gts:// URI is allowed - validate the GTS ID
289
+ const normalizedRef = ref.substring(6);
290
+
291
+ // Check for wildcards
292
+ if (normalizedRef.includes('*')) {
293
+ errors.push(`Invalid $ref at ${refPath}: wildcards are not allowed in $ref`);
294
+ } else if (!gts.isValidGtsID(normalizedRef)) {
295
+ errors.push(`Invalid $ref at ${refPath}: ${normalizedRef} is not a valid GTS identifier`);
296
+ }
297
+ } else if (ref.startsWith('gts.')) {
298
+ // Plain gts. prefix without gts:// is not allowed
299
+ errors.push(`Invalid $ref at ${refPath}: GTS references must use gts:// URI format`);
300
+ } else if (ref.startsWith('http://') || ref.startsWith('https://')) {
301
+ // External HTTP refs are not allowed (except json-schema.org for $schema)
302
+ if (!ref.includes('json-schema.org')) {
303
+ errors.push(`Invalid $ref at ${refPath}: external HTTP references are not allowed`);
304
+ }
305
+ }
306
+ }
307
+
308
+ // Recurse into nested objects
309
+ for (const [key, value] of Object.entries(obj)) {
310
+ if (key === '$ref' || key === '$$ref') continue;
311
+ if (value && typeof value === 'object') {
312
+ const nestedPath = path ? `${path}/${key}` : key;
313
+ if (Array.isArray(value)) {
314
+ value.forEach((item, idx) => {
315
+ if (item && typeof item === 'object') {
316
+ errors.push(...this.validateSchemaRefs(item, `${nestedPath}[${idx}]`));
317
+ }
318
+ });
319
+ } else {
320
+ errors.push(...this.validateSchemaRefs(value, nestedPath));
321
+ }
322
+ }
323
+ }
324
+
325
+ return errors;
326
+ }
327
+
328
+ private async handleAddEntities(
329
+ request: FastifyRequest<{ Body: any[] }>,
330
+ _reply: FastifyReply
331
+ ): Promise<OperationResult> {
332
+ try {
333
+ const entities = request.body;
334
+
335
+ if (!Array.isArray(entities)) {
336
+ return {
337
+ ok: false,
338
+ error: 'Request body must be an array of entities',
339
+ };
340
+ }
341
+
342
+ const registered: string[] = [];
343
+ const errors: string[] = [];
344
+
345
+ for (const content of entities) {
346
+ try {
347
+ const entity = createJsonEntity(content);
348
+ if (entity.id) {
349
+ this.store.register(content);
350
+ registered.push(entity.id);
351
+ } else {
352
+ errors.push('Unable to extract GTS ID from entity');
353
+ }
354
+ } catch (err) {
355
+ errors.push(err instanceof Error ? err.message : String(err));
356
+ }
357
+ }
358
+
359
+ return {
360
+ ok: errors.length === 0,
361
+ registered,
362
+ errors: errors.length > 0 ? errors : undefined,
363
+ };
364
+ } catch (error) {
365
+ return {
366
+ ok: false,
367
+ error: error instanceof Error ? error.message : String(error),
368
+ };
369
+ }
370
+ }
371
+
372
+ private async handleAddSchema(request: FastifyRequest<{ Body: any }>, reply: FastifyReply): Promise<OperationResult> {
373
+ return this.handleAddEntity(request as any, reply);
374
+ }
375
+
376
+ // OP#1 - Validate ID
377
+ private async handleValidateID(
378
+ request: FastifyRequest<{ Querystring: ValidateIDParams }>,
379
+ reply: FastifyReply
380
+ ): Promise<any> {
381
+ // Support both 'gts_id' (test spec) and 'id' (fallback)
382
+ const id = request.query.gts_id || request.query.id;
383
+
384
+ if (!id) {
385
+ reply.code(400);
386
+ throw new Error('Missing required parameter: gts_id or id');
387
+ }
388
+
389
+ return gts.validateGtsID(id);
390
+ }
391
+
392
+ // OP#2 - Extract ID
393
+ private async handleExtractID(request: FastifyRequest<{ Body: any }>, _reply: FastifyReply): Promise<any> {
394
+ // The body itself is the content
395
+ return gts.extractID(request.body);
396
+ }
397
+
398
+ // OP#3 - Parse ID
399
+ private async handleParseID(
400
+ request: FastifyRequest<{ Querystring: { gts_id?: string; id?: string } }>,
401
+ reply: FastifyReply
402
+ ): Promise<any> {
403
+ const id = request.query.gts_id || request.query.id;
404
+
405
+ if (!id) {
406
+ reply.code(400);
407
+ throw new Error('Missing required parameter: gts_id or id');
408
+ }
409
+
410
+ const result = gts.parseGtsID(id);
411
+ const isWildcard = id.includes('*');
412
+
413
+ // Convert segments to match test format (snake_case)
414
+ const segments =
415
+ result.segments?.map((seg) => ({
416
+ vendor: seg.vendor,
417
+ package: seg.package,
418
+ namespace: seg.namespace,
419
+ type: seg.type,
420
+ ver_major: seg.verMajor,
421
+ ver_minor: seg.verMinor ?? null,
422
+ is_type: seg.isType,
423
+ })) || [];
424
+
425
+ // is_schema: true if ends with ~ and not a wildcard ending with ~*
426
+ const isSchema = id.endsWith('~') && !isWildcard;
427
+
428
+ return {
429
+ id,
430
+ ok: result.ok,
431
+ segments,
432
+ error: result.error || '',
433
+ is_schema: isSchema,
434
+ is_wildcard: isWildcard,
435
+ };
436
+ }
437
+
438
+ // OP#4 - Match ID Pattern
439
+ private async handleMatchIDPattern(
440
+ request: FastifyRequest<{ Querystring: MatchPatternParams }>,
441
+ reply: FastifyReply
442
+ ): Promise<any> {
443
+ const { pattern, candidate } = request.query;
444
+
445
+ if (!pattern || !candidate) {
446
+ reply.code(400);
447
+ throw new Error('Missing required parameters: pattern, candidate');
448
+ }
449
+
450
+ return gts.matchIDPattern(candidate, pattern);
451
+ }
452
+
453
+ // OP#5 - UUID
454
+ private async handleUUID(
455
+ request: FastifyRequest<{ Querystring: { gts_id?: string; id?: string } }>,
456
+ reply: FastifyReply
457
+ ): Promise<any> {
458
+ const id = request.query.gts_id || request.query.id;
459
+
460
+ if (!id) {
461
+ reply.code(400);
462
+ throw new Error('Missing required parameter: gts_id or id');
463
+ }
464
+
465
+ return gts.idToUUID(id);
466
+ }
467
+
468
+ // OP#6 - Validate Instance
469
+ private async handleValidateInstance(
470
+ request: FastifyRequest<{ Body: ValidateInstanceBody }>,
471
+ reply: FastifyReply
472
+ ): Promise<any> {
473
+ const { instance_id } = request.body;
474
+
475
+ if (!instance_id) {
476
+ reply.code(400);
477
+ throw new Error('Missing required field: instance_id');
478
+ }
479
+
480
+ return this.store.validateInstance(instance_id);
481
+ }
482
+
483
+ // OP#7 - Resolve Relationships
484
+ private async handleResolveRelationships(
485
+ request: FastifyRequest<{ Querystring: ResolveRelationshipsParams }>,
486
+ reply: FastifyReply
487
+ ): Promise<any> {
488
+ const { gts_id } = request.query;
489
+
490
+ if (!gts_id) {
491
+ reply.code(400);
492
+ throw new Error('Missing required parameter: gts_id');
493
+ }
494
+
495
+ return this.store.resolveRelationships(gts_id);
496
+ }
497
+
498
+ // OP#8 - Compatibility
499
+ private async handleCompatibility(
500
+ request: FastifyRequest<{ Querystring: CompatibilityParams }>,
501
+ reply: FastifyReply
502
+ ): Promise<any> {
503
+ const { old_schema_id, new_schema_id, mode = 'full' } = request.query;
504
+
505
+ if (!old_schema_id || !new_schema_id) {
506
+ reply.code(400);
507
+ throw new Error('Missing required parameters: old_schema_id, new_schema_id');
508
+ }
509
+
510
+ // Call the store's checkCompatibility directly to get the correct response format
511
+ return this.store['store'].checkCompatibility(old_schema_id, new_schema_id, mode);
512
+ }
513
+
514
+ // OP#9 - Cast
515
+ private async handleCast(request: FastifyRequest<{ Body: CastBody }>, reply: FastifyReply): Promise<any> {
516
+ const { instance_id, to_schema_id } = request.body;
517
+
518
+ if (!instance_id || !to_schema_id) {
519
+ reply.code(400);
520
+ throw new Error('Missing required fields: instance_id, to_schema_id');
521
+ }
522
+
523
+ // Call the store's castInstance directly to get the correct response format
524
+ return this.store['store'].castInstance(instance_id, to_schema_id);
525
+ }
526
+
527
+ // OP#10 - Query
528
+ private async handleQuery(request: FastifyRequest<{ Querystring: QueryParams }>, reply: FastifyReply): Promise<any> {
529
+ const { expr, limit } = request.query;
530
+
531
+ if (!expr) {
532
+ reply.code(400);
533
+ throw new Error('Missing required parameter: expr');
534
+ }
535
+
536
+ const queryLimit = limit !== undefined ? Number(limit) : 100;
537
+ const result = this.store.query(expr, queryLimit);
538
+
539
+ // Tests expect 'results' not 'items', 'error' field first if present, and 'limit' field always
540
+ const response: any = {};
541
+
542
+ if (result.error) {
543
+ response.error = result.error;
544
+ } else {
545
+ response.error = '';
546
+ }
547
+
548
+ response.count = result.count;
549
+ response.limit = queryLimit;
550
+ response.results = result.items;
551
+
552
+ return response;
553
+ }
554
+
555
+ // OP#11 - Attribute Access
556
+ private async handleAttribute(
557
+ request: FastifyRequest<{ Querystring: { gts_with_path?: string; gts_id?: string; path?: string } }>,
558
+ reply: FastifyReply
559
+ ): Promise<any> {
560
+ // Handle both formats: gts_with_path or separate gts_id + path
561
+ let gtsId: string;
562
+ let path: string;
563
+
564
+ if (request.query.gts_with_path) {
565
+ // Split on @ symbol to extract gts_id and path
566
+ const parts = request.query.gts_with_path.split('@');
567
+ if (parts.length !== 2) {
568
+ // If no @ symbol, treat the whole thing as gts_id with empty path
569
+ // This will result in resolved=false
570
+ gtsId = request.query.gts_with_path;
571
+ path = '';
572
+ } else {
573
+ gtsId = parts[0];
574
+ path = parts[1];
575
+ }
576
+ } else if (request.query.gts_id && request.query.path) {
577
+ gtsId = request.query.gts_id;
578
+ path = request.query.path;
579
+ } else {
580
+ reply.code(400);
581
+ throw new Error('Missing required parameters: gts_with_path or (gts_id, path)');
582
+ }
583
+
584
+ return this.store['store'].getAttribute(gtsId, path);
585
+ }
586
+
587
+ // OpenAPI Specification
588
+ private async handleOpenAPI(): Promise<any> {
589
+ return {
590
+ openapi: '3.0.0',
591
+ info: {
592
+ title: 'GTS Server',
593
+ version: '0.1.0',
594
+ description: 'GTS (Global Type System) HTTP API',
595
+ },
596
+ servers: [
597
+ {
598
+ url: `http://${this.config.host}:${this.config.port}`,
599
+ description: 'GTS Server',
600
+ },
601
+ ],
602
+ paths: this.getOpenAPIPaths(),
603
+ components: this.getOpenAPIComponents(),
604
+ };
605
+ }
606
+
607
+ private getOpenAPIPaths(): any {
608
+ return {
609
+ '/entities': {
610
+ get: {
611
+ summary: 'Get all entities in the registry',
612
+ operationId: 'getEntities',
613
+ parameters: [
614
+ {
615
+ name: 'limit',
616
+ in: 'query',
617
+ description: 'Maximum number of entities to return',
618
+ schema: { type: 'integer', default: 100, minimum: 1, maximum: 1000 },
619
+ },
620
+ ],
621
+ responses: {
622
+ 200: {
623
+ description: 'List of entity IDs',
624
+ content: {
625
+ 'application/json': {
626
+ schema: {
627
+ type: 'object',
628
+ properties: {
629
+ count: { type: 'integer' },
630
+ items: { type: 'array', items: { type: 'string' } },
631
+ },
632
+ },
633
+ },
634
+ },
635
+ },
636
+ },
637
+ },
638
+ post: {
639
+ summary: 'Add a new entity',
640
+ operationId: 'addEntity',
641
+ requestBody: {
642
+ required: true,
643
+ content: {
644
+ 'application/json': {
645
+ schema: { type: 'object' },
646
+ },
647
+ },
648
+ },
649
+ responses: {
650
+ 200: {
651
+ description: 'Operation result',
652
+ content: {
653
+ 'application/json': {
654
+ schema: { $ref: '#/components/schemas/OperationResult' },
655
+ },
656
+ },
657
+ },
658
+ },
659
+ },
660
+ },
661
+ '/validate-id': {
662
+ get: {
663
+ summary: 'Validate a GTS ID',
664
+ operationId: 'validateID',
665
+ parameters: [
666
+ {
667
+ name: 'id',
668
+ in: 'query',
669
+ required: true,
670
+ description: 'GTS ID to validate',
671
+ schema: { type: 'string' },
672
+ },
673
+ ],
674
+ responses: {
675
+ 200: {
676
+ description: 'Validation result',
677
+ content: {
678
+ 'application/json': {
679
+ schema: { $ref: '#/components/schemas/ValidationResult' },
680
+ },
681
+ },
682
+ },
683
+ },
684
+ },
685
+ },
686
+ '/query': {
687
+ get: {
688
+ summary: 'Query entities using GTS query language',
689
+ operationId: 'query',
690
+ parameters: [
691
+ {
692
+ name: 'expr',
693
+ in: 'query',
694
+ required: true,
695
+ description: 'Query expression',
696
+ schema: { type: 'string' },
697
+ },
698
+ {
699
+ name: 'limit',
700
+ in: 'query',
701
+ description: 'Maximum number of results',
702
+ schema: { type: 'integer' },
703
+ },
704
+ ],
705
+ responses: {
706
+ 200: {
707
+ description: 'Query results',
708
+ content: {
709
+ 'application/json': {
710
+ schema: { $ref: '#/components/schemas/QueryResult' },
711
+ },
712
+ },
713
+ },
714
+ },
715
+ },
716
+ },
717
+ };
718
+ }
719
+
720
+ private getOpenAPIComponents(): any {
721
+ return {
722
+ schemas: {
723
+ OperationResult: {
724
+ type: 'object',
725
+ properties: {
726
+ ok: { type: 'boolean' },
727
+ error: { type: 'string' },
728
+ },
729
+ required: ['ok'],
730
+ },
731
+ ValidationResult: {
732
+ type: 'object',
733
+ properties: {
734
+ id: { type: 'string' },
735
+ ok: { type: 'boolean' },
736
+ valid: { type: 'boolean' },
737
+ error: { type: 'string' },
738
+ },
739
+ required: ['id', 'ok'],
740
+ },
741
+ QueryResult: {
742
+ type: 'object',
743
+ properties: {
744
+ query: { type: 'string' },
745
+ count: { type: 'integer' },
746
+ items: { type: 'array', items: { type: 'string' } },
747
+ error: { type: 'string' },
748
+ },
749
+ required: ['query', 'count', 'items'],
750
+ },
751
+ },
752
+ };
753
+ }
754
+
755
+ public async start(): Promise<void> {
756
+ try {
757
+ const address = await this.fastify.listen({
758
+ port: this.config.port,
759
+ host: this.config.host,
760
+ });
761
+ console.log(`GTS server listening on ${address}`);
762
+ } catch (err) {
763
+ console.error('Error starting server:', err);
764
+ process.exit(1);
765
+ }
766
+ }
767
+
768
+ public async stop(): Promise<void> {
769
+ await this.fastify.close();
770
+ }
771
+ }