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