@globaltypesystem/gts-ts 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gts.d.ts.map +1 -1
- package/dist/gts.js +37 -4
- package/dist/gts.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +1 -12
- package/dist/server/server.js.map +1 -1
- package/dist/store.d.ts +9 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +369 -4
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/gts.ts +40 -5
- package/src/index.ts +22 -0
- package/src/server/server.ts +1 -12
- package/src/store.ts +413 -4
- package/src/types.ts +1 -0
package/src/store.ts
CHANGED
|
@@ -95,9 +95,13 @@ export class GtsStore {
|
|
|
95
95
|
|
|
96
96
|
validateInstance(gtsId: string): ValidationResult {
|
|
97
97
|
try {
|
|
98
|
-
|
|
98
|
+
let objId: string = gtsId;
|
|
99
|
+
if (Gts.isValidGtsID(gtsId)) {
|
|
100
|
+
const gid = Gts.parseGtsID(gtsId);
|
|
101
|
+
objId = gid.id;
|
|
102
|
+
}
|
|
99
103
|
|
|
100
|
-
const obj = this.get(
|
|
104
|
+
const obj = this.get(objId);
|
|
101
105
|
if (!obj) {
|
|
102
106
|
return {
|
|
103
107
|
id: gtsId,
|
|
@@ -1096,8 +1100,8 @@ export class GtsStore {
|
|
|
1096
1100
|
// Find parent reference in allOf
|
|
1097
1101
|
const parentRef = this.findParentRef(content);
|
|
1098
1102
|
if (!parentRef) {
|
|
1099
|
-
// Base schema with no parent →
|
|
1100
|
-
return
|
|
1103
|
+
// Base schema with no parent → still validate traits
|
|
1104
|
+
return this.validateSchemaTraits(schemaId);
|
|
1101
1105
|
}
|
|
1102
1106
|
|
|
1103
1107
|
// Resolve parent entity
|
|
@@ -1110,6 +1114,12 @@ export class GtsStore {
|
|
|
1110
1114
|
return { id: schemaId, ok: false, error: `Parent entity is not a schema: ${parentId}` };
|
|
1111
1115
|
}
|
|
1112
1116
|
|
|
1117
|
+
// Detect cyclic $$ref / $ref references in the schema's own content
|
|
1118
|
+
const cycleError = this.detectRefCycle(schemaId, content, new Set([schemaId]));
|
|
1119
|
+
if (cycleError) {
|
|
1120
|
+
return { id: schemaId, ok: false, error: cycleError };
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1113
1123
|
// Resolve parent's effective (fully flattened) schema
|
|
1114
1124
|
const resolvedParent = this.resolveSchemaFully(parentEntity.content);
|
|
1115
1125
|
|
|
@@ -1122,9 +1132,408 @@ export class GtsStore {
|
|
|
1122
1132
|
return { id: schemaId, ok: false, error: errors.join('; ') };
|
|
1123
1133
|
}
|
|
1124
1134
|
|
|
1135
|
+
// OP#13: Validate schema traits across the inheritance chain
|
|
1136
|
+
const traitsResult = this.validateSchemaTraits(schemaId);
|
|
1137
|
+
if (!traitsResult.ok) {
|
|
1138
|
+
return traitsResult;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
return { id: schemaId, ok: true, error: '' };
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// OP#13: Validate schema traits across the inheritance chain
|
|
1145
|
+
private validateSchemaTraits(schemaId: string): ValidationResult {
|
|
1146
|
+
// Build the chain of schema IDs from base to leaf
|
|
1147
|
+
const chain = this.buildSchemaChain(schemaId);
|
|
1148
|
+
|
|
1149
|
+
// Collect trait schemas and trait values from each level, tracking immutability
|
|
1150
|
+
const traitSchemas: any[] = [];
|
|
1151
|
+
const mergedTraits: Record<string, any> = {};
|
|
1152
|
+
const lockedTraits = new Set<string>();
|
|
1153
|
+
const knownDefaults = new Map<string, any>();
|
|
1154
|
+
|
|
1155
|
+
for (const chainSchemaId of chain) {
|
|
1156
|
+
const entity = this.get(chainSchemaId);
|
|
1157
|
+
if (!entity || !entity.content) continue;
|
|
1158
|
+
|
|
1159
|
+
// Collect trait schemas from this level and track which properties this level introduces
|
|
1160
|
+
const prevSchemaCount = traitSchemas.length;
|
|
1161
|
+
this.collectTraitSchemas(entity.content, traitSchemas);
|
|
1162
|
+
const levelSchemaProps = new Set<string>();
|
|
1163
|
+
for (const ts of traitSchemas.slice(prevSchemaCount)) {
|
|
1164
|
+
if (typeof ts === 'object' && ts !== null && typeof ts.properties === 'object' && ts.properties !== null) {
|
|
1165
|
+
for (const [propName, propSchema] of Object.entries(ts.properties)) {
|
|
1166
|
+
levelSchemaProps.add(propName);
|
|
1167
|
+
// Detect default override: ancestor default cannot be changed by descendant
|
|
1168
|
+
if (
|
|
1169
|
+
typeof propSchema === 'object' &&
|
|
1170
|
+
propSchema !== null &&
|
|
1171
|
+
'default' in (propSchema as Record<string, any>)
|
|
1172
|
+
) {
|
|
1173
|
+
const newDefault = (propSchema as Record<string, any>).default;
|
|
1174
|
+
if (knownDefaults.has(propName)) {
|
|
1175
|
+
const oldDefault = knownDefaults.get(propName);
|
|
1176
|
+
if (JSON.stringify(oldDefault) !== JSON.stringify(newDefault)) {
|
|
1177
|
+
return {
|
|
1178
|
+
id: schemaId,
|
|
1179
|
+
ok: false,
|
|
1180
|
+
error: `trait schema default for '${propName}' in '${chainSchemaId}' overrides default set by ancestor`,
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
} else {
|
|
1184
|
+
knownDefaults.set(propName, newDefault);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Collect trait values from this level
|
|
1192
|
+
const levelTraits: Record<string, any> = {};
|
|
1193
|
+
this.collectTraitValues(entity.content, levelTraits);
|
|
1194
|
+
|
|
1195
|
+
// Check immutability: trait values set by ancestor are locked unless
|
|
1196
|
+
// this level also introduces a trait schema covering that property
|
|
1197
|
+
for (const [k, v] of Object.entries(levelTraits)) {
|
|
1198
|
+
if (k in mergedTraits && JSON.stringify(mergedTraits[k]) !== JSON.stringify(v) && lockedTraits.has(k)) {
|
|
1199
|
+
return {
|
|
1200
|
+
id: schemaId,
|
|
1201
|
+
ok: false,
|
|
1202
|
+
error: `trait '${k}' in '${chainSchemaId}' overrides value set by ancestor`,
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// Mark trait values as locked or unlocked based on whether this level
|
|
1208
|
+
// also introduced a trait schema covering the property
|
|
1209
|
+
for (const k of Object.keys(levelTraits)) {
|
|
1210
|
+
if (levelSchemaProps.has(k)) {
|
|
1211
|
+
lockedTraits.delete(k);
|
|
1212
|
+
} else {
|
|
1213
|
+
lockedTraits.add(k);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
Object.assign(mergedTraits, levelTraits);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// If no trait schemas in the chain, nothing to validate
|
|
1221
|
+
if (traitSchemas.length === 0) {
|
|
1222
|
+
if (Object.keys(mergedTraits).length > 0) {
|
|
1223
|
+
return {
|
|
1224
|
+
id: schemaId,
|
|
1225
|
+
ok: false,
|
|
1226
|
+
error: 'x-gts-traits values provided but no x-gts-traits-schema is defined in the inheritance chain',
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
return { id: schemaId, ok: true, error: '' };
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Validate each trait schema
|
|
1233
|
+
for (let i = 0; i < traitSchemas.length; i++) {
|
|
1234
|
+
const ts = traitSchemas[i];
|
|
1235
|
+
|
|
1236
|
+
// Check: trait schema must have type "object" (or no type, which defaults to object)
|
|
1237
|
+
if (typeof ts === 'object' && ts !== null && ts.type && ts.type !== 'object') {
|
|
1238
|
+
return {
|
|
1239
|
+
id: schemaId,
|
|
1240
|
+
ok: false,
|
|
1241
|
+
error: `x-gts-traits-schema must have type "object", got "${ts.type}"`,
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// Check: trait schema must not contain x-gts-traits
|
|
1246
|
+
if (typeof ts === 'object' && ts !== null && ts['x-gts-traits']) {
|
|
1247
|
+
return {
|
|
1248
|
+
id: schemaId,
|
|
1249
|
+
ok: false,
|
|
1250
|
+
error: 'x-gts-traits-schema must not contain x-gts-traits',
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Resolve $ref inside trait schemas and check for cycles
|
|
1256
|
+
const resolvedTraitSchemas: any[] = [];
|
|
1257
|
+
for (const ts of traitSchemas) {
|
|
1258
|
+
try {
|
|
1259
|
+
const resolved = this.resolveTraitSchemaRefs(ts, new Set());
|
|
1260
|
+
resolvedTraitSchemas.push(resolved);
|
|
1261
|
+
} catch (e) {
|
|
1262
|
+
return {
|
|
1263
|
+
id: schemaId,
|
|
1264
|
+
ok: false,
|
|
1265
|
+
error: e instanceof Error ? e.message : String(e),
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Build effective trait schema (allOf composition)
|
|
1271
|
+
let effectiveSchema: any;
|
|
1272
|
+
if (resolvedTraitSchemas.length === 1) {
|
|
1273
|
+
effectiveSchema = resolvedTraitSchemas[0];
|
|
1274
|
+
} else {
|
|
1275
|
+
effectiveSchema = {
|
|
1276
|
+
type: 'object',
|
|
1277
|
+
allOf: resolvedTraitSchemas,
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Apply defaults from trait schema to merged traits
|
|
1282
|
+
const effectiveTraits = this.applyTraitDefaults(effectiveSchema, mergedTraits);
|
|
1283
|
+
|
|
1284
|
+
// Validate effective traits against effective schema using AJV
|
|
1285
|
+
try {
|
|
1286
|
+
const normalizedSchema = this.normalizeSchema(effectiveSchema);
|
|
1287
|
+
const validate = this.ajv.compile(normalizedSchema);
|
|
1288
|
+
const isValid = validate(effectiveTraits);
|
|
1289
|
+
if (!isValid) {
|
|
1290
|
+
const errors =
|
|
1291
|
+
validate.errors?.map((e) => `${e.instancePath} ${e.message}`).join('; ') || 'Trait validation failed';
|
|
1292
|
+
return { id: schemaId, ok: false, error: `trait validation: ${errors}` };
|
|
1293
|
+
}
|
|
1294
|
+
} catch (e) {
|
|
1295
|
+
return {
|
|
1296
|
+
id: schemaId,
|
|
1297
|
+
ok: false,
|
|
1298
|
+
error: `failed to compile trait schema: ${e instanceof Error ? e.message : String(e)}`,
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// Check for unresolved trait properties (no value and no default)
|
|
1303
|
+
const allProps = this.collectAllTraitProperties(effectiveSchema);
|
|
1304
|
+
for (const [propName, propSchema] of Object.entries(allProps)) {
|
|
1305
|
+
const hasValue = propName in effectiveTraits;
|
|
1306
|
+
const hasDefault = typeof propSchema === 'object' && propSchema !== null && 'default' in propSchema;
|
|
1307
|
+
if (!hasValue && !hasDefault) {
|
|
1308
|
+
return {
|
|
1309
|
+
id: schemaId,
|
|
1310
|
+
ok: false,
|
|
1311
|
+
error: `trait property '${propName}' is not resolved: no value provided and no default defined`,
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1125
1316
|
return { id: schemaId, ok: true, error: '' };
|
|
1126
1317
|
}
|
|
1127
1318
|
|
|
1319
|
+
// OP#13: Entity-level traits validation
|
|
1320
|
+
validateEntityTraits(entityId: string): ValidationResult {
|
|
1321
|
+
const entity = this.get(entityId);
|
|
1322
|
+
if (!entity) {
|
|
1323
|
+
return { id: entityId, ok: false, error: `Entity not found: ${entityId}` };
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
if (!entity.isSchema) {
|
|
1327
|
+
return { id: entityId, ok: true, error: '' };
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// Build the chain for this schema
|
|
1331
|
+
const chain = this.buildSchemaChain(entityId);
|
|
1332
|
+
|
|
1333
|
+
const traitSchemas: any[] = [];
|
|
1334
|
+
let hasTraitValues = false;
|
|
1335
|
+
|
|
1336
|
+
for (const chainSchemaId of chain) {
|
|
1337
|
+
const chainEntity = this.get(chainSchemaId);
|
|
1338
|
+
if (!chainEntity || !chainEntity.content) continue;
|
|
1339
|
+
|
|
1340
|
+
this.collectTraitSchemas(chainEntity.content, traitSchemas);
|
|
1341
|
+
|
|
1342
|
+
const levelTraits: Record<string, any> = {};
|
|
1343
|
+
this.collectTraitValues(chainEntity.content, levelTraits);
|
|
1344
|
+
if (Object.keys(levelTraits).length > 0) {
|
|
1345
|
+
hasTraitValues = true;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
if (traitSchemas.length === 0) {
|
|
1350
|
+
return { id: entityId, ok: true, error: '' };
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// If trait schemas exist but no trait values, entity is incomplete
|
|
1354
|
+
if (!hasTraitValues) {
|
|
1355
|
+
return {
|
|
1356
|
+
id: entityId,
|
|
1357
|
+
ok: false,
|
|
1358
|
+
error: 'Entity defines x-gts-traits-schema but no x-gts-traits values are provided',
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Each trait schema must have additionalProperties: false (closed)
|
|
1363
|
+
for (const ts of traitSchemas) {
|
|
1364
|
+
if (typeof ts === 'object' && ts !== null) {
|
|
1365
|
+
if (ts.additionalProperties !== false) {
|
|
1366
|
+
return {
|
|
1367
|
+
id: entityId,
|
|
1368
|
+
ok: false,
|
|
1369
|
+
error: 'Trait schema must set additionalProperties: false for entity validation',
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
return { id: entityId, ok: true, error: '' };
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// Build the schema chain from base to leaf for a given schema ID
|
|
1379
|
+
private buildSchemaChain(schemaId: string): string[] {
|
|
1380
|
+
// Parse the schema ID to get segments
|
|
1381
|
+
try {
|
|
1382
|
+
const gtsId = Gts.parseGtsID(schemaId);
|
|
1383
|
+
const segments = gtsId.segments;
|
|
1384
|
+
const chain: string[] = [];
|
|
1385
|
+
|
|
1386
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1387
|
+
const id =
|
|
1388
|
+
'gts.' +
|
|
1389
|
+
segments
|
|
1390
|
+
.slice(0, i + 1)
|
|
1391
|
+
.map((s) => s.segment)
|
|
1392
|
+
.join('');
|
|
1393
|
+
chain.push(id);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
return chain;
|
|
1397
|
+
} catch {
|
|
1398
|
+
return [schemaId];
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// Collect x-gts-traits-schema from a schema content (recursing into allOf)
|
|
1403
|
+
private collectTraitSchemas(content: any, out: any[], depth: number = 0): void {
|
|
1404
|
+
if (depth > 64 || typeof content !== 'object' || content === null) return;
|
|
1405
|
+
|
|
1406
|
+
if (content['x-gts-traits-schema'] !== undefined) {
|
|
1407
|
+
out.push(content['x-gts-traits-schema']);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
if (Array.isArray(content.allOf)) {
|
|
1411
|
+
for (const item of content.allOf) {
|
|
1412
|
+
this.collectTraitSchemas(item, out, depth + 1);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// Collect x-gts-traits from a schema content (recursing into allOf)
|
|
1418
|
+
private collectTraitValues(content: any, merged: Record<string, any>, depth: number = 0): void {
|
|
1419
|
+
if (depth > 64 || typeof content !== 'object' || content === null) return;
|
|
1420
|
+
|
|
1421
|
+
if (typeof content['x-gts-traits'] === 'object' && content['x-gts-traits'] !== null) {
|
|
1422
|
+
Object.assign(merged, content['x-gts-traits']);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
if (Array.isArray(content.allOf)) {
|
|
1426
|
+
for (const item of content.allOf) {
|
|
1427
|
+
this.collectTraitValues(item, merged, depth + 1);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// Resolve $ref inside a trait schema, detecting cycles
|
|
1433
|
+
private resolveTraitSchemaRefs(schema: any, visited: Set<string>, depth: number = 0): any {
|
|
1434
|
+
if (depth > 64) return schema;
|
|
1435
|
+
if (typeof schema !== 'object' || schema === null) return schema;
|
|
1436
|
+
|
|
1437
|
+
const result: any = {};
|
|
1438
|
+
|
|
1439
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
1440
|
+
if (key === '$$ref' || key === '$ref') {
|
|
1441
|
+
const refUri = value as string;
|
|
1442
|
+
const refId = refUri.startsWith(GTS_URI_PREFIX) ? refUri.substring(GTS_URI_PREFIX.length) : refUri;
|
|
1443
|
+
|
|
1444
|
+
if (visited.has(refId)) {
|
|
1445
|
+
throw new Error(`Cyclic reference detected in trait schema: ${refId}`);
|
|
1446
|
+
}
|
|
1447
|
+
visited.add(refId);
|
|
1448
|
+
|
|
1449
|
+
const refEntity = this.get(refId);
|
|
1450
|
+
if (!refEntity || !refEntity.content) {
|
|
1451
|
+
throw new Error(`Unresolvable trait schema reference: ${refUri}`);
|
|
1452
|
+
}
|
|
1453
|
+
const resolved = this.resolveTraitSchemaRefs(refEntity.content, visited, depth + 1);
|
|
1454
|
+
// Merge resolved content into result
|
|
1455
|
+
for (const [rk, rv] of Object.entries(resolved)) {
|
|
1456
|
+
if (rk !== '$id' && rk !== '$$id' && rk !== '$schema' && rk !== '$$schema') {
|
|
1457
|
+
result[rk] = rv;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
if (key === 'allOf' && Array.isArray(value)) {
|
|
1464
|
+
result.allOf = (value as any[]).map((item) => this.resolveTraitSchemaRefs(item, visited, depth + 1));
|
|
1465
|
+
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
1466
|
+
result[key] = this.resolveTraitSchemaRefs(value, new Set(visited), depth + 1);
|
|
1467
|
+
} else {
|
|
1468
|
+
result[key] = value;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
return result;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// Apply defaults from trait schema to trait values
|
|
1476
|
+
private applyTraitDefaults(schema: any, traits: Record<string, any>): Record<string, any> {
|
|
1477
|
+
const result = { ...traits };
|
|
1478
|
+
const props = this.collectAllTraitProperties(schema);
|
|
1479
|
+
|
|
1480
|
+
for (const [propName, propSchema] of Object.entries(props)) {
|
|
1481
|
+
if (!(propName in result) && typeof propSchema === 'object' && propSchema !== null && 'default' in propSchema) {
|
|
1482
|
+
result[propName] = propSchema.default;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
return result;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// Collect all properties from a trait schema (handling allOf composition)
|
|
1490
|
+
private collectAllTraitProperties(schema: any, depth: number = 0): Record<string, any> {
|
|
1491
|
+
const props: Record<string, any> = {};
|
|
1492
|
+
if (depth > 64 || typeof schema !== 'object' || schema === null) return props;
|
|
1493
|
+
|
|
1494
|
+
if (typeof schema.properties === 'object' && schema.properties !== null) {
|
|
1495
|
+
Object.assign(props, schema.properties);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
if (Array.isArray(schema.allOf)) {
|
|
1499
|
+
for (const item of schema.allOf) {
|
|
1500
|
+
Object.assign(props, this.collectAllTraitProperties(item, depth + 1));
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
return props;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// Detect cyclic $$ref/$ref references reachable from a schema's content
|
|
1508
|
+
private detectRefCycle(originId: string, content: any, visited: Set<string>, depth: number = 0): string | null {
|
|
1509
|
+
if (depth > 64 || !content || typeof content !== 'object') return null;
|
|
1510
|
+
|
|
1511
|
+
// Check direct ref on this object
|
|
1512
|
+
const ref = content['$$ref'] || content['$ref'];
|
|
1513
|
+
if (typeof ref === 'string') {
|
|
1514
|
+
const refId = ref.startsWith(GTS_URI_PREFIX) ? ref.substring(GTS_URI_PREFIX.length) : ref;
|
|
1515
|
+
if (visited.has(refId)) {
|
|
1516
|
+
return `Cyclic reference detected: ${refId}`;
|
|
1517
|
+
}
|
|
1518
|
+
const refEntity = this.get(refId);
|
|
1519
|
+
if (refEntity && refEntity.content) {
|
|
1520
|
+
visited.add(refId);
|
|
1521
|
+
const inner = this.detectRefCycle(originId, refEntity.content, visited, depth + 1);
|
|
1522
|
+
if (inner) return inner;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Recurse into allOf
|
|
1527
|
+
if (Array.isArray(content.allOf)) {
|
|
1528
|
+
for (const sub of content.allOf) {
|
|
1529
|
+
const inner = this.detectRefCycle(originId, sub, visited, depth + 1);
|
|
1530
|
+
if (inner) return inner;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
return null;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1128
1537
|
private findParentRef(schema: any): string | null {
|
|
1129
1538
|
if (!schema || !schema.allOf || !Array.isArray(schema.allOf)) {
|
|
1130
1539
|
return null;
|