@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
package/dist/store.js
ADDED
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.GtsStore = void 0;
|
|
7
|
+
exports.createJsonEntity = createJsonEntity;
|
|
8
|
+
const ajv_1 = __importDefault(require("ajv"));
|
|
9
|
+
const types_1 = require("./types");
|
|
10
|
+
const gts_1 = require("./gts");
|
|
11
|
+
const extract_1 = require("./extract");
|
|
12
|
+
const x_gts_ref_1 = require("./x-gts-ref");
|
|
13
|
+
class GtsStore {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.byId = new Map();
|
|
16
|
+
this.config = {
|
|
17
|
+
validateRefs: config?.validateRefs ?? false,
|
|
18
|
+
strictMode: config?.strictMode ?? false,
|
|
19
|
+
};
|
|
20
|
+
this.ajv = new ajv_1.default({
|
|
21
|
+
strict: false,
|
|
22
|
+
validateSchema: false,
|
|
23
|
+
addUsedSchema: false,
|
|
24
|
+
loadSchema: this.loadSchema.bind(this),
|
|
25
|
+
validateFormats: false, // Disable format validation to match Go implementation
|
|
26
|
+
});
|
|
27
|
+
// Don't add format validators since Go uses lenient validation
|
|
28
|
+
}
|
|
29
|
+
async loadSchema(uri) {
|
|
30
|
+
const normalizedUri = uri.startsWith(types_1.GTS_URI_PREFIX) ? uri.substring(types_1.GTS_URI_PREFIX.length) : uri;
|
|
31
|
+
if (gts_1.Gts.isValidGtsID(normalizedUri)) {
|
|
32
|
+
const entity = this.get(normalizedUri);
|
|
33
|
+
if (entity && entity.isSchema) {
|
|
34
|
+
return entity.content;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`Unresolvable GTS reference: ${uri}`);
|
|
38
|
+
}
|
|
39
|
+
register(entity) {
|
|
40
|
+
if (this.config.validateRefs) {
|
|
41
|
+
for (const ref of entity.references) {
|
|
42
|
+
if (!this.byId.has(ref)) {
|
|
43
|
+
throw new Error(`Unresolved reference: ${ref}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
this.byId.set(entity.id, entity);
|
|
48
|
+
// If this is a schema, add it to AJV for reference resolution
|
|
49
|
+
if (entity.isSchema && entity.content) {
|
|
50
|
+
try {
|
|
51
|
+
const normalizedSchema = this.normalizeSchema(entity.content);
|
|
52
|
+
// Set $id to the GTS ID if not already set
|
|
53
|
+
if (!normalizedSchema.$id) {
|
|
54
|
+
normalizedSchema.$id = entity.id;
|
|
55
|
+
}
|
|
56
|
+
this.ajv.addSchema(normalizedSchema, entity.id);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
// Ignore errors adding schema - it might already exist or be invalid
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
get(id) {
|
|
64
|
+
return this.byId.get(id);
|
|
65
|
+
}
|
|
66
|
+
getAll() {
|
|
67
|
+
return Array.from(this.byId.values());
|
|
68
|
+
}
|
|
69
|
+
query(pattern, limit) {
|
|
70
|
+
const results = [];
|
|
71
|
+
const maxResults = limit ?? Number.MAX_SAFE_INTEGER;
|
|
72
|
+
for (const [id] of this.byId) {
|
|
73
|
+
if (results.length >= maxResults)
|
|
74
|
+
break;
|
|
75
|
+
const matchResult = gts_1.Gts.matchIDPattern(id, pattern);
|
|
76
|
+
if (matchResult.match) {
|
|
77
|
+
results.push(id);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return results;
|
|
81
|
+
}
|
|
82
|
+
validateInstance(gtsId) {
|
|
83
|
+
try {
|
|
84
|
+
const gid = gts_1.Gts.parseGtsID(gtsId);
|
|
85
|
+
const obj = this.get(gid.id);
|
|
86
|
+
if (!obj) {
|
|
87
|
+
return {
|
|
88
|
+
id: gtsId,
|
|
89
|
+
ok: false,
|
|
90
|
+
valid: false,
|
|
91
|
+
error: `Entity not found: ${gtsId}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (!obj.schemaId) {
|
|
95
|
+
return {
|
|
96
|
+
id: gtsId,
|
|
97
|
+
ok: false,
|
|
98
|
+
valid: false,
|
|
99
|
+
error: `No schema found for instance: ${gtsId}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const schemaEntity = this.get(obj.schemaId);
|
|
103
|
+
if (!schemaEntity) {
|
|
104
|
+
return {
|
|
105
|
+
id: gtsId,
|
|
106
|
+
ok: false,
|
|
107
|
+
valid: false,
|
|
108
|
+
error: `Schema not found: ${obj.schemaId}`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (!schemaEntity.isSchema) {
|
|
112
|
+
return {
|
|
113
|
+
id: gtsId,
|
|
114
|
+
ok: false,
|
|
115
|
+
valid: false,
|
|
116
|
+
error: `Entity '${obj.schemaId}' is not a schema`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const validate = this.ajv.compile(this.normalizeSchema(schemaEntity.content));
|
|
120
|
+
const isValid = validate(obj.content);
|
|
121
|
+
if (!isValid) {
|
|
122
|
+
const errors = validate.errors
|
|
123
|
+
?.map((e) => {
|
|
124
|
+
if (e.keyword === 'required') {
|
|
125
|
+
return `${e.instancePath || '/'} must have required property '${e.params?.missingProperty}'`;
|
|
126
|
+
}
|
|
127
|
+
return `${e.instancePath} ${e.message}`;
|
|
128
|
+
})
|
|
129
|
+
.join('; ') || 'Validation failed';
|
|
130
|
+
return {
|
|
131
|
+
id: gtsId,
|
|
132
|
+
ok: false,
|
|
133
|
+
valid: false,
|
|
134
|
+
error: errors,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// Validate x-gts-ref constraints
|
|
138
|
+
const xGtsRefValidator = new x_gts_ref_1.XGtsRefValidator(this);
|
|
139
|
+
const xGtsRefErrors = xGtsRefValidator.validateInstance(obj.content, schemaEntity.content);
|
|
140
|
+
if (xGtsRefErrors.length > 0) {
|
|
141
|
+
const errorMsgs = xGtsRefErrors.map((err) => err.reason).join('; ');
|
|
142
|
+
return {
|
|
143
|
+
id: gtsId,
|
|
144
|
+
ok: false,
|
|
145
|
+
valid: false,
|
|
146
|
+
error: `x-gts-ref validation failed: ${errorMsgs}`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
id: gtsId,
|
|
151
|
+
ok: true,
|
|
152
|
+
valid: true,
|
|
153
|
+
error: '',
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
return {
|
|
158
|
+
id: gtsId,
|
|
159
|
+
ok: false,
|
|
160
|
+
valid: false,
|
|
161
|
+
error: error instanceof Error ? error.message : String(error),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
normalizeSchema(schema) {
|
|
166
|
+
return this.normalizeSchemaRecursive(schema);
|
|
167
|
+
}
|
|
168
|
+
normalizeSchemaRecursive(obj) {
|
|
169
|
+
if (obj === null || typeof obj !== 'object') {
|
|
170
|
+
return obj;
|
|
171
|
+
}
|
|
172
|
+
if (Array.isArray(obj)) {
|
|
173
|
+
return obj.map((item) => this.normalizeSchemaRecursive(item));
|
|
174
|
+
}
|
|
175
|
+
const normalized = {};
|
|
176
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
177
|
+
let newKey = key;
|
|
178
|
+
let newValue = value;
|
|
179
|
+
// Convert $$ prefixed keys to $ prefixed keys
|
|
180
|
+
switch (key) {
|
|
181
|
+
case '$$id':
|
|
182
|
+
newKey = '$id';
|
|
183
|
+
break;
|
|
184
|
+
case '$$schema':
|
|
185
|
+
newKey = '$schema';
|
|
186
|
+
break;
|
|
187
|
+
case '$$ref':
|
|
188
|
+
newKey = '$ref';
|
|
189
|
+
break;
|
|
190
|
+
case '$$defs':
|
|
191
|
+
newKey = '$defs';
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
// Recursively normalize nested objects
|
|
195
|
+
if (value && typeof value === 'object') {
|
|
196
|
+
newValue = this.normalizeSchemaRecursive(value);
|
|
197
|
+
}
|
|
198
|
+
normalized[newKey] = newValue;
|
|
199
|
+
}
|
|
200
|
+
// Normalize $id values
|
|
201
|
+
if (normalized['$id'] && typeof normalized['$id'] === 'string') {
|
|
202
|
+
if (normalized['$id'].startsWith(types_1.GTS_URI_PREFIX)) {
|
|
203
|
+
normalized['$id'] = normalized['$id'].substring(types_1.GTS_URI_PREFIX.length);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Normalize $ref values
|
|
207
|
+
if (normalized['$ref'] && typeof normalized['$ref'] === 'string') {
|
|
208
|
+
if (normalized['$ref'].startsWith(types_1.GTS_URI_PREFIX)) {
|
|
209
|
+
normalized['$ref'] = normalized['$ref'].substring(types_1.GTS_URI_PREFIX.length);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return normalized;
|
|
213
|
+
}
|
|
214
|
+
resolveRelationships(gtsId) {
|
|
215
|
+
const seen = new Set();
|
|
216
|
+
return this.buildSchemaGraphNode(gtsId, seen);
|
|
217
|
+
}
|
|
218
|
+
buildSchemaGraphNode(gtsId, seen) {
|
|
219
|
+
const node = {
|
|
220
|
+
id: gtsId,
|
|
221
|
+
};
|
|
222
|
+
// Check for cycles
|
|
223
|
+
if (seen.has(gtsId)) {
|
|
224
|
+
return node;
|
|
225
|
+
}
|
|
226
|
+
seen.add(gtsId);
|
|
227
|
+
// Get the entity from store
|
|
228
|
+
const entity = this.get(gtsId);
|
|
229
|
+
if (!entity) {
|
|
230
|
+
node.errors = ['Entity not found'];
|
|
231
|
+
return node;
|
|
232
|
+
}
|
|
233
|
+
// Process GTS references found in the entity
|
|
234
|
+
const refs = this.extractGtsReferences(entity.content);
|
|
235
|
+
const nodeRefs = {};
|
|
236
|
+
for (const ref of refs) {
|
|
237
|
+
// Skip self-references
|
|
238
|
+
if (ref.id === gtsId) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
// Skip JSON Schema meta-schema references
|
|
242
|
+
if (this.isJsonSchemaUrl(ref.id)) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
// Recursively build node for this reference
|
|
246
|
+
nodeRefs[ref.sourcePath] = this.buildSchemaGraphNode(ref.id, seen);
|
|
247
|
+
}
|
|
248
|
+
if (Object.keys(nodeRefs).length > 0) {
|
|
249
|
+
node.refs = nodeRefs;
|
|
250
|
+
}
|
|
251
|
+
// Process schema ID if present
|
|
252
|
+
if (entity.schemaId) {
|
|
253
|
+
if (!this.isJsonSchemaUrl(entity.schemaId)) {
|
|
254
|
+
node.schema_id = this.buildSchemaGraphNode(entity.schemaId, seen);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else if (!entity.isSchema) {
|
|
258
|
+
// Instance without schema ID is an error
|
|
259
|
+
node.errors = node.errors || [];
|
|
260
|
+
node.errors.push('Schema not recognized');
|
|
261
|
+
}
|
|
262
|
+
return node;
|
|
263
|
+
}
|
|
264
|
+
extractGtsReferences(content) {
|
|
265
|
+
const refs = [];
|
|
266
|
+
const seen = new Set();
|
|
267
|
+
const walkAndCollectRefs = (node, path) => {
|
|
268
|
+
if (node === null || node === undefined) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// Check if current node is a GTS ID string
|
|
272
|
+
if (typeof node === 'string') {
|
|
273
|
+
if (gts_1.Gts.isValidGtsID(node)) {
|
|
274
|
+
const sourcePath = path || 'root';
|
|
275
|
+
const key = `${node}|${sourcePath}`;
|
|
276
|
+
if (!seen.has(key)) {
|
|
277
|
+
refs.push({ id: node, sourcePath });
|
|
278
|
+
seen.add(key);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
// Recurse into object
|
|
284
|
+
if (typeof node === 'object' && !Array.isArray(node)) {
|
|
285
|
+
for (const [k, v] of Object.entries(node)) {
|
|
286
|
+
const nextPath = path ? `${path}.${k}` : k;
|
|
287
|
+
walkAndCollectRefs(v, nextPath);
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
// Recurse into array
|
|
292
|
+
if (Array.isArray(node)) {
|
|
293
|
+
for (let i = 0; i < node.length; i++) {
|
|
294
|
+
const nextPath = path ? `${path}[${i}]` : `[${i}]`;
|
|
295
|
+
walkAndCollectRefs(node[i], nextPath);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
walkAndCollectRefs(content, '');
|
|
300
|
+
return refs;
|
|
301
|
+
}
|
|
302
|
+
isJsonSchemaUrl(s) {
|
|
303
|
+
return (s.startsWith('http://') || s.startsWith('https://')) && s.includes('json-schema.org');
|
|
304
|
+
}
|
|
305
|
+
checkCompatibility(oldSchemaId, newSchemaId, _mode) {
|
|
306
|
+
const oldEntity = this.get(oldSchemaId);
|
|
307
|
+
const newEntity = this.get(newSchemaId);
|
|
308
|
+
if (!oldEntity || !newEntity) {
|
|
309
|
+
return {
|
|
310
|
+
from: oldSchemaId,
|
|
311
|
+
to: newSchemaId,
|
|
312
|
+
old: oldSchemaId,
|
|
313
|
+
new: newSchemaId,
|
|
314
|
+
direction: 'unknown',
|
|
315
|
+
added_properties: [],
|
|
316
|
+
removed_properties: [],
|
|
317
|
+
changed_properties: [],
|
|
318
|
+
is_fully_compatible: false,
|
|
319
|
+
is_backward_compatible: false,
|
|
320
|
+
is_forward_compatible: false,
|
|
321
|
+
incompatibility_reasons: [],
|
|
322
|
+
backward_errors: ['Schema not found'],
|
|
323
|
+
forward_errors: ['Schema not found'],
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const oldSchema = oldEntity.content;
|
|
327
|
+
const newSchema = newEntity.content;
|
|
328
|
+
if (!oldSchema || !newSchema) {
|
|
329
|
+
return {
|
|
330
|
+
from: oldSchemaId,
|
|
331
|
+
to: newSchemaId,
|
|
332
|
+
old: oldSchemaId,
|
|
333
|
+
new: newSchemaId,
|
|
334
|
+
direction: 'unknown',
|
|
335
|
+
added_properties: [],
|
|
336
|
+
removed_properties: [],
|
|
337
|
+
changed_properties: [],
|
|
338
|
+
is_fully_compatible: false,
|
|
339
|
+
is_backward_compatible: false,
|
|
340
|
+
is_forward_compatible: false,
|
|
341
|
+
incompatibility_reasons: [],
|
|
342
|
+
backward_errors: ['Invalid schema content'],
|
|
343
|
+
forward_errors: ['Invalid schema content'],
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
// Check compatibility
|
|
347
|
+
const { isBackward, backwardErrors } = this.checkBackwardCompatibility(oldSchema, newSchema);
|
|
348
|
+
const { isForward, forwardErrors } = this.checkForwardCompatibility(oldSchema, newSchema);
|
|
349
|
+
// Determine direction
|
|
350
|
+
const direction = this.inferDirection(oldSchemaId, newSchemaId);
|
|
351
|
+
return {
|
|
352
|
+
from: oldSchemaId,
|
|
353
|
+
to: newSchemaId,
|
|
354
|
+
old: oldSchemaId,
|
|
355
|
+
new: newSchemaId,
|
|
356
|
+
direction,
|
|
357
|
+
added_properties: [],
|
|
358
|
+
removed_properties: [],
|
|
359
|
+
changed_properties: [],
|
|
360
|
+
is_fully_compatible: isBackward && isForward,
|
|
361
|
+
is_backward_compatible: isBackward,
|
|
362
|
+
is_forward_compatible: isForward,
|
|
363
|
+
incompatibility_reasons: [],
|
|
364
|
+
backward_errors: backwardErrors,
|
|
365
|
+
forward_errors: forwardErrors,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
inferDirection(fromId, toId) {
|
|
369
|
+
try {
|
|
370
|
+
const fromGtsId = gts_1.Gts.parseGtsID(fromId);
|
|
371
|
+
const toGtsId = gts_1.Gts.parseGtsID(toId);
|
|
372
|
+
if (!fromGtsId.segments.length || !toGtsId.segments.length) {
|
|
373
|
+
return 'unknown';
|
|
374
|
+
}
|
|
375
|
+
const fromSeg = fromGtsId.segments[fromGtsId.segments.length - 1];
|
|
376
|
+
const toSeg = toGtsId.segments[toGtsId.segments.length - 1];
|
|
377
|
+
if (fromSeg.verMinor !== undefined && toSeg.verMinor !== undefined) {
|
|
378
|
+
if (toSeg.verMinor > fromSeg.verMinor) {
|
|
379
|
+
return 'up';
|
|
380
|
+
}
|
|
381
|
+
if (toSeg.verMinor < fromSeg.verMinor) {
|
|
382
|
+
return 'down';
|
|
383
|
+
}
|
|
384
|
+
return 'none';
|
|
385
|
+
}
|
|
386
|
+
return 'unknown';
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return 'unknown';
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
checkBackwardCompatibility(oldSchema, newSchema) {
|
|
393
|
+
return this.checkSchemaCompatibility(oldSchema, newSchema, true);
|
|
394
|
+
}
|
|
395
|
+
checkForwardCompatibility(oldSchema, newSchema) {
|
|
396
|
+
return this.checkSchemaCompatibility(oldSchema, newSchema, false);
|
|
397
|
+
}
|
|
398
|
+
checkSchemaCompatibility(oldSchema, newSchema, checkBackward) {
|
|
399
|
+
const errors = [];
|
|
400
|
+
// Flatten schemas to handle allOf
|
|
401
|
+
const oldFlat = this.flattenSchema(oldSchema);
|
|
402
|
+
const newFlat = this.flattenSchema(newSchema);
|
|
403
|
+
const oldProps = oldFlat.properties || {};
|
|
404
|
+
const newProps = newFlat.properties || {};
|
|
405
|
+
const oldRequired = new Set(oldFlat.required || []);
|
|
406
|
+
const newRequired = new Set(newFlat.required || []);
|
|
407
|
+
// Check required properties changes
|
|
408
|
+
if (checkBackward) {
|
|
409
|
+
// Backward: cannot add required properties
|
|
410
|
+
const newlyRequired = Array.from(newRequired).filter((p) => !oldRequired.has(p));
|
|
411
|
+
if (newlyRequired.length > 0) {
|
|
412
|
+
errors.push(`Added required properties: ${newlyRequired.join(', ')}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
// Forward: cannot remove required properties
|
|
417
|
+
const removedRequired = Array.from(oldRequired).filter((p) => !newRequired.has(p));
|
|
418
|
+
if (removedRequired.length > 0) {
|
|
419
|
+
errors.push(`Removed required properties: ${removedRequired.join(', ')}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// Check properties that exist in both schemas
|
|
423
|
+
const commonProps = Object.keys(oldProps).filter((k) => k in newProps);
|
|
424
|
+
for (const prop of commonProps) {
|
|
425
|
+
const oldPropSchema = oldProps[prop] || {};
|
|
426
|
+
const newPropSchema = newProps[prop] || {};
|
|
427
|
+
// Check if type changed
|
|
428
|
+
const oldType = oldPropSchema.type;
|
|
429
|
+
const newType = newPropSchema.type;
|
|
430
|
+
if (oldType && newType && oldType !== newType) {
|
|
431
|
+
errors.push(`Property '${prop}' type changed from ${oldType} to ${newType}`);
|
|
432
|
+
}
|
|
433
|
+
// Check enum constraints
|
|
434
|
+
const oldEnum = oldPropSchema.enum || [];
|
|
435
|
+
const newEnum = newPropSchema.enum || [];
|
|
436
|
+
if (oldEnum.length > 0 && newEnum.length > 0) {
|
|
437
|
+
const oldEnumSet = new Set(oldEnum);
|
|
438
|
+
const newEnumSet = new Set(newEnum);
|
|
439
|
+
if (checkBackward) {
|
|
440
|
+
// Backward: cannot add enum values
|
|
441
|
+
const addedEnumValues = newEnum.filter((v) => !oldEnumSet.has(v));
|
|
442
|
+
if (addedEnumValues.length > 0) {
|
|
443
|
+
errors.push(`Property '${prop}' added enum values: ${addedEnumValues.join(', ')}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// Forward: cannot remove enum values
|
|
448
|
+
const removedEnumValues = oldEnum.filter((v) => !newEnumSet.has(v));
|
|
449
|
+
if (removedEnumValues.length > 0) {
|
|
450
|
+
errors.push(`Property '${prop}' removed enum values: ${removedEnumValues.join(', ')}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// Check constraint compatibility
|
|
455
|
+
errors.push(...this.checkConstraintCompatibility(prop, oldPropSchema, newPropSchema, checkBackward));
|
|
456
|
+
// Recursively check nested object properties
|
|
457
|
+
if (oldType === 'object' && newType === 'object') {
|
|
458
|
+
const nestedResult = this.checkSchemaCompatibility(oldPropSchema, newPropSchema, checkBackward);
|
|
459
|
+
const nestedErrors = checkBackward ? nestedResult.backwardErrors : nestedResult.forwardErrors;
|
|
460
|
+
if (nestedErrors) {
|
|
461
|
+
errors.push(...nestedErrors.map((e) => `Property '${prop}': ${e}`));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// Recursively check array item schemas
|
|
465
|
+
if (oldType === 'array' && newType === 'array' && oldPropSchema.items && newPropSchema.items) {
|
|
466
|
+
const itemsResult = this.checkSchemaCompatibility(oldPropSchema.items, newPropSchema.items, checkBackward);
|
|
467
|
+
const itemsErrors = checkBackward ? itemsResult.backwardErrors : itemsResult.forwardErrors;
|
|
468
|
+
if (itemsErrors) {
|
|
469
|
+
errors.push(...itemsErrors.map((e) => `Property '${prop}' array items: ${e}`));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (checkBackward) {
|
|
474
|
+
return { isBackward: errors.length === 0, backwardErrors: errors };
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
return { isForward: errors.length === 0, forwardErrors: errors };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
checkConstraintCompatibility(prop, oldPropSchema, newPropSchema, checkTightening) {
|
|
481
|
+
const errors = [];
|
|
482
|
+
const propType = oldPropSchema.type;
|
|
483
|
+
// Numeric constraints
|
|
484
|
+
if (propType === 'number' || propType === 'integer') {
|
|
485
|
+
errors.push(...this.checkMinMaxConstraint(prop, oldPropSchema, newPropSchema, 'minimum', 'maximum', checkTightening));
|
|
486
|
+
}
|
|
487
|
+
// String constraints
|
|
488
|
+
if (propType === 'string') {
|
|
489
|
+
errors.push(...this.checkMinMaxConstraint(prop, oldPropSchema, newPropSchema, 'minLength', 'maxLength', checkTightening));
|
|
490
|
+
}
|
|
491
|
+
// Array constraints
|
|
492
|
+
if (propType === 'array') {
|
|
493
|
+
errors.push(...this.checkMinMaxConstraint(prop, oldPropSchema, newPropSchema, 'minItems', 'maxItems', checkTightening));
|
|
494
|
+
}
|
|
495
|
+
return errors;
|
|
496
|
+
}
|
|
497
|
+
checkMinMaxConstraint(prop, oldSchema, newSchema, minKey, maxKey, checkTightening) {
|
|
498
|
+
const errors = [];
|
|
499
|
+
const oldMin = oldSchema[minKey];
|
|
500
|
+
const newMin = newSchema[minKey];
|
|
501
|
+
const oldMax = oldSchema[maxKey];
|
|
502
|
+
const newMax = newSchema[maxKey];
|
|
503
|
+
// Check minimum constraint
|
|
504
|
+
if (checkTightening) {
|
|
505
|
+
// Backward: cannot increase minimum (tighten)
|
|
506
|
+
if (oldMin !== undefined && newMin !== undefined && newMin > oldMin) {
|
|
507
|
+
errors.push(`Property '${prop}' ${minKey} increased from ${oldMin} to ${newMin}`);
|
|
508
|
+
}
|
|
509
|
+
else if (oldMin === undefined && newMin !== undefined) {
|
|
510
|
+
errors.push(`Property '${prop}' added ${minKey} constraint: ${newMin}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
// Forward: cannot decrease minimum (relax)
|
|
515
|
+
if (oldMin !== undefined && newMin !== undefined && newMin < oldMin) {
|
|
516
|
+
errors.push(`Property '${prop}' ${minKey} decreased from ${oldMin} to ${newMin}`);
|
|
517
|
+
}
|
|
518
|
+
else if (oldMin !== undefined && newMin === undefined) {
|
|
519
|
+
errors.push(`Property '${prop}' removed ${minKey} constraint`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
// Check maximum constraint
|
|
523
|
+
if (checkTightening) {
|
|
524
|
+
// Backward: cannot decrease maximum (tighten)
|
|
525
|
+
if (oldMax !== undefined && newMax !== undefined && newMax < oldMax) {
|
|
526
|
+
errors.push(`Property '${prop}' ${maxKey} decreased from ${oldMax} to ${newMax}`);
|
|
527
|
+
}
|
|
528
|
+
else if (oldMax === undefined && newMax !== undefined) {
|
|
529
|
+
errors.push(`Property '${prop}' added ${maxKey} constraint: ${newMax}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
// Forward: cannot increase maximum (relax)
|
|
534
|
+
if (oldMax !== undefined && newMax !== undefined && newMax > oldMax) {
|
|
535
|
+
errors.push(`Property '${prop}' ${maxKey} increased from ${oldMax} to ${newMax}`);
|
|
536
|
+
}
|
|
537
|
+
else if (oldMax !== undefined && newMax === undefined) {
|
|
538
|
+
errors.push(`Property '${prop}' removed ${maxKey} constraint`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return errors;
|
|
542
|
+
}
|
|
543
|
+
flattenSchema(schema) {
|
|
544
|
+
const result = {
|
|
545
|
+
properties: {},
|
|
546
|
+
required: [],
|
|
547
|
+
};
|
|
548
|
+
// Merge allOf schemas
|
|
549
|
+
if (schema.allOf && Array.isArray(schema.allOf)) {
|
|
550
|
+
for (const subSchema of schema.allOf) {
|
|
551
|
+
const flattened = this.flattenSchema(subSchema);
|
|
552
|
+
// Merge properties
|
|
553
|
+
Object.assign(result.properties, flattened.properties || {});
|
|
554
|
+
// Merge required
|
|
555
|
+
if (flattened.required && Array.isArray(flattened.required)) {
|
|
556
|
+
result.required.push(...flattened.required);
|
|
557
|
+
}
|
|
558
|
+
// Preserve additionalProperties
|
|
559
|
+
if (flattened.additionalProperties !== undefined) {
|
|
560
|
+
result.additionalProperties = flattened.additionalProperties;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// Add direct properties
|
|
565
|
+
if (schema.properties) {
|
|
566
|
+
Object.assign(result.properties, schema.properties);
|
|
567
|
+
}
|
|
568
|
+
// Add direct required
|
|
569
|
+
if (schema.required && Array.isArray(schema.required)) {
|
|
570
|
+
result.required.push(...schema.required);
|
|
571
|
+
}
|
|
572
|
+
// Top level additionalProperties overrides
|
|
573
|
+
if (schema.additionalProperties !== undefined) {
|
|
574
|
+
result.additionalProperties = schema.additionalProperties;
|
|
575
|
+
}
|
|
576
|
+
return result;
|
|
577
|
+
}
|
|
578
|
+
castInstance(instanceId, toSchemaId) {
|
|
579
|
+
try {
|
|
580
|
+
// Get instance entity
|
|
581
|
+
const instanceEntity = this.get(instanceId);
|
|
582
|
+
if (!instanceEntity) {
|
|
583
|
+
return {
|
|
584
|
+
instance_id: instanceId,
|
|
585
|
+
to_schema_id: toSchemaId,
|
|
586
|
+
ok: false,
|
|
587
|
+
error: `Entity not found: ${instanceId}`,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
// Get target schema
|
|
591
|
+
const toSchema = this.get(toSchemaId);
|
|
592
|
+
if (!toSchema) {
|
|
593
|
+
return {
|
|
594
|
+
instance_id: instanceId,
|
|
595
|
+
to_schema_id: toSchemaId,
|
|
596
|
+
ok: false,
|
|
597
|
+
error: `Schema not found: ${toSchemaId}`,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
// Determine source schema
|
|
601
|
+
let fromSchemaId;
|
|
602
|
+
let fromSchema;
|
|
603
|
+
if (instanceEntity.isSchema) {
|
|
604
|
+
// Not allowed to cast directly from a schema
|
|
605
|
+
return {
|
|
606
|
+
instance_id: instanceId,
|
|
607
|
+
to_schema_id: toSchemaId,
|
|
608
|
+
ok: false,
|
|
609
|
+
error: 'Source must be an instance, not a schema',
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
// Casting an instance - need to find its schema
|
|
614
|
+
fromSchemaId = instanceEntity.schemaId;
|
|
615
|
+
if (!fromSchemaId) {
|
|
616
|
+
return {
|
|
617
|
+
instance_id: instanceId,
|
|
618
|
+
to_schema_id: toSchemaId,
|
|
619
|
+
ok: false,
|
|
620
|
+
error: `Schema not found for instance: ${instanceId}`,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
// Don't try to get a JSON Schema URL as a GTS entity
|
|
624
|
+
if (fromSchemaId.startsWith('http://') || fromSchemaId.startsWith('https://')) {
|
|
625
|
+
return {
|
|
626
|
+
instance_id: instanceId,
|
|
627
|
+
to_schema_id: toSchemaId,
|
|
628
|
+
ok: false,
|
|
629
|
+
error: `Cannot cast instance with schema ${fromSchemaId}`,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
fromSchema = this.get(fromSchemaId);
|
|
633
|
+
if (!fromSchema) {
|
|
634
|
+
return {
|
|
635
|
+
instance_id: instanceId,
|
|
636
|
+
to_schema_id: toSchemaId,
|
|
637
|
+
ok: false,
|
|
638
|
+
error: `Schema not found: ${fromSchemaId}`,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// Get content
|
|
643
|
+
const instanceContent = instanceEntity.content;
|
|
644
|
+
const fromSchemaContent = fromSchema.content;
|
|
645
|
+
const toSchemaContent = toSchema.content;
|
|
646
|
+
// Perform the cast
|
|
647
|
+
return this.performCast(instanceId, toSchemaId, instanceContent, fromSchemaContent, toSchemaContent);
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
return {
|
|
651
|
+
instance_id: instanceId,
|
|
652
|
+
to_schema_id: toSchemaId,
|
|
653
|
+
ok: false,
|
|
654
|
+
error: error instanceof Error ? error.message : String(error),
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
performCast(fromInstanceId, toSchemaId, fromInstanceContent, fromSchemaContent, toSchemaContent) {
|
|
659
|
+
// Flatten target schema to merge allOf
|
|
660
|
+
const targetSchema = this.flattenSchema(toSchemaContent);
|
|
661
|
+
// Determine direction
|
|
662
|
+
const direction = this.inferDirection(fromInstanceId, toSchemaId);
|
|
663
|
+
// Determine which is old/new based on direction
|
|
664
|
+
let oldSchema;
|
|
665
|
+
let newSchema;
|
|
666
|
+
switch (direction) {
|
|
667
|
+
case 'up':
|
|
668
|
+
oldSchema = fromSchemaContent;
|
|
669
|
+
newSchema = toSchemaContent;
|
|
670
|
+
break;
|
|
671
|
+
case 'down':
|
|
672
|
+
oldSchema = toSchemaContent;
|
|
673
|
+
newSchema = fromSchemaContent;
|
|
674
|
+
break;
|
|
675
|
+
default:
|
|
676
|
+
oldSchema = fromSchemaContent;
|
|
677
|
+
newSchema = toSchemaContent;
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
// Check compatibility
|
|
681
|
+
const { isBackward, backwardErrors } = this.checkBackwardCompatibility(oldSchema, newSchema);
|
|
682
|
+
const { isForward, forwardErrors } = this.checkForwardCompatibility(oldSchema, newSchema);
|
|
683
|
+
// Apply casting rules to transform the instance
|
|
684
|
+
const { casted, added, removed, incompatibilityReasons } = this.castInstanceToSchema(this.deepCopy(fromInstanceContent), targetSchema, '');
|
|
685
|
+
// Validate the casted instance against the target schema
|
|
686
|
+
let isFullyCompatible = false;
|
|
687
|
+
if (casted) {
|
|
688
|
+
try {
|
|
689
|
+
const modifiedSchema = this.removeGtsConstConstraints(toSchemaContent);
|
|
690
|
+
const validate = this.ajv.compile(this.normalizeSchema(modifiedSchema));
|
|
691
|
+
const isValid = validate(casted);
|
|
692
|
+
if (!isValid) {
|
|
693
|
+
const errors = validate.errors?.map((e) => `${e.instancePath} ${e.message}`).join('; ') || 'Validation failed';
|
|
694
|
+
incompatibilityReasons.push(errors);
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
isFullyCompatible = true;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
catch (err) {
|
|
701
|
+
incompatibilityReasons.push(err instanceof Error ? err.message : String(err));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
from: fromInstanceId,
|
|
706
|
+
to: toSchemaId,
|
|
707
|
+
old: fromInstanceId,
|
|
708
|
+
new: toSchemaId,
|
|
709
|
+
direction,
|
|
710
|
+
added_properties: this.deduplicate(added),
|
|
711
|
+
removed_properties: this.deduplicate(removed),
|
|
712
|
+
changed_properties: [],
|
|
713
|
+
is_fully_compatible: isFullyCompatible,
|
|
714
|
+
is_backward_compatible: isBackward,
|
|
715
|
+
is_forward_compatible: isForward,
|
|
716
|
+
incompatibility_reasons: incompatibilityReasons,
|
|
717
|
+
backward_errors: backwardErrors,
|
|
718
|
+
forward_errors: forwardErrors,
|
|
719
|
+
casted_entity: casted,
|
|
720
|
+
instance_id: fromInstanceId,
|
|
721
|
+
to_schema_id: toSchemaId,
|
|
722
|
+
ok: isFullyCompatible,
|
|
723
|
+
error: isFullyCompatible ? '' : incompatibilityReasons.join('; '),
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
castInstanceToSchema(instance, schema, basePath) {
|
|
727
|
+
const added = [];
|
|
728
|
+
const removed = [];
|
|
729
|
+
const incompatibilityReasons = [];
|
|
730
|
+
if (!instance || typeof instance !== 'object' || Array.isArray(instance)) {
|
|
731
|
+
incompatibilityReasons.push('Instance must be an object for casting');
|
|
732
|
+
return { casted: null, added, removed, incompatibilityReasons };
|
|
733
|
+
}
|
|
734
|
+
const targetProps = schema.properties || {};
|
|
735
|
+
const required = new Set(schema.required || []);
|
|
736
|
+
const additional = schema.additionalProperties !== false;
|
|
737
|
+
// Start from current values
|
|
738
|
+
const result = this.deepCopy(instance);
|
|
739
|
+
// 1) Ensure required properties exist (fill defaults if provided)
|
|
740
|
+
for (const reqProp of Array.from(required)) {
|
|
741
|
+
if (!(reqProp in result)) {
|
|
742
|
+
const propSchema = targetProps[reqProp];
|
|
743
|
+
if (propSchema && propSchema.default !== undefined) {
|
|
744
|
+
result[reqProp] = this.deepCopy(propSchema.default);
|
|
745
|
+
const path = this.buildPath(basePath, reqProp);
|
|
746
|
+
added.push(path);
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
const path = this.buildPath(basePath, reqProp);
|
|
750
|
+
incompatibilityReasons.push(`Missing required property '${path}' and no default is defined`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
// 2) For optional properties with defaults, set if missing
|
|
755
|
+
for (const [prop, propSchema] of Object.entries(targetProps)) {
|
|
756
|
+
if (required.has(prop)) {
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (!(prop in result)) {
|
|
760
|
+
const ps = propSchema;
|
|
761
|
+
if (ps.default !== undefined) {
|
|
762
|
+
result[prop] = this.deepCopy(ps.default);
|
|
763
|
+
const path = this.buildPath(basePath, prop);
|
|
764
|
+
added.push(path);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
// 2.5) Update const values to match target schema (for GTS ID fields)
|
|
769
|
+
for (const [prop, propSchema] of Object.entries(targetProps)) {
|
|
770
|
+
const ps = propSchema;
|
|
771
|
+
if (ps.const !== undefined) {
|
|
772
|
+
const constVal = ps.const;
|
|
773
|
+
const existingVal = result[prop];
|
|
774
|
+
if (typeof constVal === 'string' && typeof existingVal === 'string') {
|
|
775
|
+
// Only update if both are GTS IDs and they differ
|
|
776
|
+
if (gts_1.Gts.isValidGtsID(constVal) && gts_1.Gts.isValidGtsID(existingVal)) {
|
|
777
|
+
if (existingVal !== constVal) {
|
|
778
|
+
result[prop] = constVal;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
// 3) Remove properties not in target schema when additionalProperties is false
|
|
785
|
+
if (!additional) {
|
|
786
|
+
for (const prop of Object.keys(result)) {
|
|
787
|
+
if (!(prop in targetProps)) {
|
|
788
|
+
delete result[prop];
|
|
789
|
+
const path = this.buildPath(basePath, prop);
|
|
790
|
+
removed.push(path);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// 4) Recurse into nested object properties
|
|
795
|
+
for (const [prop, propSchema] of Object.entries(targetProps)) {
|
|
796
|
+
const val = result[prop];
|
|
797
|
+
if (val === undefined) {
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
const ps = propSchema;
|
|
801
|
+
const propType = ps.type;
|
|
802
|
+
// Handle nested objects
|
|
803
|
+
if (propType === 'object') {
|
|
804
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
805
|
+
const nestedSchema = this.effectiveObjectSchema(ps);
|
|
806
|
+
const nestedResult = this.castInstanceToSchema(val, nestedSchema, this.buildPath(basePath, prop));
|
|
807
|
+
result[prop] = nestedResult.casted;
|
|
808
|
+
added.push(...nestedResult.added);
|
|
809
|
+
removed.push(...nestedResult.removed);
|
|
810
|
+
incompatibilityReasons.push(...nestedResult.incompatibilityReasons);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
// Handle arrays of objects
|
|
814
|
+
if (propType === 'array') {
|
|
815
|
+
if (Array.isArray(val)) {
|
|
816
|
+
const itemsSchema = ps.items;
|
|
817
|
+
if (itemsSchema && itemsSchema.type === 'object') {
|
|
818
|
+
const nestedSchema = this.effectiveObjectSchema(itemsSchema);
|
|
819
|
+
const newList = [];
|
|
820
|
+
for (let idx = 0; idx < val.length; idx++) {
|
|
821
|
+
const item = val[idx];
|
|
822
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
823
|
+
const nestedResult = this.castInstanceToSchema(item, nestedSchema, this.buildPath(basePath, `${prop}[${idx}]`));
|
|
824
|
+
newList.push(nestedResult.casted);
|
|
825
|
+
added.push(...nestedResult.added);
|
|
826
|
+
removed.push(...nestedResult.removed);
|
|
827
|
+
incompatibilityReasons.push(...nestedResult.incompatibilityReasons);
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
newList.push(item);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
result[prop] = newList;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return { casted: result, added, removed, incompatibilityReasons };
|
|
839
|
+
}
|
|
840
|
+
effectiveObjectSchema(schema) {
|
|
841
|
+
if (!schema) {
|
|
842
|
+
return {};
|
|
843
|
+
}
|
|
844
|
+
// If it has properties or required directly, use it
|
|
845
|
+
if (schema.properties || schema.required) {
|
|
846
|
+
return schema;
|
|
847
|
+
}
|
|
848
|
+
// Check allOf for object schemas
|
|
849
|
+
if (schema.allOf && Array.isArray(schema.allOf)) {
|
|
850
|
+
for (const part of schema.allOf) {
|
|
851
|
+
if (part.properties || part.required) {
|
|
852
|
+
return part;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return schema;
|
|
857
|
+
}
|
|
858
|
+
removeGtsConstConstraints(schema) {
|
|
859
|
+
if (schema === null || schema === undefined) {
|
|
860
|
+
return schema;
|
|
861
|
+
}
|
|
862
|
+
if (typeof schema === 'object' && !Array.isArray(schema)) {
|
|
863
|
+
const result = {};
|
|
864
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
865
|
+
if (key === 'const') {
|
|
866
|
+
if (typeof value === 'string' && gts_1.Gts.isValidGtsID(value)) {
|
|
867
|
+
// Replace const with type constraint instead
|
|
868
|
+
result.type = 'string';
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
result[key] = this.removeGtsConstConstraints(value);
|
|
873
|
+
}
|
|
874
|
+
return result;
|
|
875
|
+
}
|
|
876
|
+
if (Array.isArray(schema)) {
|
|
877
|
+
return schema.map((item) => this.removeGtsConstConstraints(item));
|
|
878
|
+
}
|
|
879
|
+
return schema;
|
|
880
|
+
}
|
|
881
|
+
buildPath(base, prop) {
|
|
882
|
+
if (!base) {
|
|
883
|
+
return prop;
|
|
884
|
+
}
|
|
885
|
+
// Handle array indices that already have brackets
|
|
886
|
+
if (prop.startsWith('[')) {
|
|
887
|
+
return base + prop;
|
|
888
|
+
}
|
|
889
|
+
return base + '.' + prop;
|
|
890
|
+
}
|
|
891
|
+
deepCopy(obj) {
|
|
892
|
+
if (obj === null || obj === undefined) {
|
|
893
|
+
return obj;
|
|
894
|
+
}
|
|
895
|
+
if (typeof obj !== 'object') {
|
|
896
|
+
return obj;
|
|
897
|
+
}
|
|
898
|
+
if (Array.isArray(obj)) {
|
|
899
|
+
return obj.map((item) => this.deepCopy(item));
|
|
900
|
+
}
|
|
901
|
+
const result = {};
|
|
902
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
903
|
+
result[key] = this.deepCopy(value);
|
|
904
|
+
}
|
|
905
|
+
return result;
|
|
906
|
+
}
|
|
907
|
+
deduplicate(arr) {
|
|
908
|
+
const unique = Array.from(new Set(arr));
|
|
909
|
+
return unique.sort();
|
|
910
|
+
}
|
|
911
|
+
getAttribute(gtsId, path) {
|
|
912
|
+
const entity = this.get(gtsId);
|
|
913
|
+
if (!entity) {
|
|
914
|
+
return {
|
|
915
|
+
gts_id: gtsId,
|
|
916
|
+
path,
|
|
917
|
+
resolved: false,
|
|
918
|
+
error: `Entity not found: ${gtsId}`,
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
const value = this.getNestedValue(entity.content, path);
|
|
922
|
+
return {
|
|
923
|
+
gts_id: gtsId,
|
|
924
|
+
path,
|
|
925
|
+
resolved: value !== undefined,
|
|
926
|
+
value,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
getNestedValue(obj, path) {
|
|
930
|
+
// Split path by dots but handle array notation
|
|
931
|
+
const parts = [];
|
|
932
|
+
let current = '';
|
|
933
|
+
let inBracket = false;
|
|
934
|
+
for (let i = 0; i < path.length; i++) {
|
|
935
|
+
const char = path[i];
|
|
936
|
+
if (char === '[') {
|
|
937
|
+
if (current) {
|
|
938
|
+
parts.push(current);
|
|
939
|
+
current = '';
|
|
940
|
+
}
|
|
941
|
+
inBracket = true;
|
|
942
|
+
}
|
|
943
|
+
else if (char === ']') {
|
|
944
|
+
if (current) {
|
|
945
|
+
parts.push(`[${current}]`);
|
|
946
|
+
current = '';
|
|
947
|
+
}
|
|
948
|
+
inBracket = false;
|
|
949
|
+
}
|
|
950
|
+
else if (char === '.' && !inBracket) {
|
|
951
|
+
if (current) {
|
|
952
|
+
parts.push(current);
|
|
953
|
+
current = '';
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
current += char;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (current) {
|
|
961
|
+
parts.push(current);
|
|
962
|
+
}
|
|
963
|
+
let result = obj;
|
|
964
|
+
for (const part of parts) {
|
|
965
|
+
if (result === null || result === undefined) {
|
|
966
|
+
return undefined;
|
|
967
|
+
}
|
|
968
|
+
// Handle array index notation
|
|
969
|
+
if (part.startsWith('[') && part.endsWith(']')) {
|
|
970
|
+
const index = parseInt(part.slice(1, -1), 10);
|
|
971
|
+
if (Array.isArray(result) && !isNaN(index)) {
|
|
972
|
+
result = result[index];
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
return undefined;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
// Regular property access
|
|
980
|
+
if (typeof result === 'object' && part in result) {
|
|
981
|
+
result = result[part];
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
return undefined;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return result;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
exports.GtsStore = GtsStore;
|
|
992
|
+
function createJsonEntity(content, _config) {
|
|
993
|
+
const extractResult = extract_1.GtsExtractor.extractID(content);
|
|
994
|
+
const references = new Set();
|
|
995
|
+
findReferences(content, references);
|
|
996
|
+
return {
|
|
997
|
+
id: extractResult.id,
|
|
998
|
+
schemaId: extractResult.schema_id,
|
|
999
|
+
content,
|
|
1000
|
+
isSchema: extractResult.is_schema,
|
|
1001
|
+
references,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
function findReferences(obj, refs, visited = new Set()) {
|
|
1005
|
+
if (!obj || typeof obj !== 'object' || visited.has(obj)) {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
visited.add(obj);
|
|
1009
|
+
if ('$ref' in obj && typeof obj['$ref'] === 'string') {
|
|
1010
|
+
const ref = obj['$ref'];
|
|
1011
|
+
const normalized = ref.startsWith(types_1.GTS_URI_PREFIX) ? ref.substring(types_1.GTS_URI_PREFIX.length) : ref;
|
|
1012
|
+
if (gts_1.Gts.isValidGtsID(normalized)) {
|
|
1013
|
+
refs.add(normalized);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if ('x-gts-ref' in obj && typeof obj['x-gts-ref'] === 'string') {
|
|
1017
|
+
const ref = obj['x-gts-ref'];
|
|
1018
|
+
if (gts_1.Gts.isValidGtsID(ref)) {
|
|
1019
|
+
refs.add(ref);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
for (const value of Object.values(obj)) {
|
|
1023
|
+
findReferences(value, refs, visited);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
//# sourceMappingURL=store.js.map
|