@globaltypesystem/gts-ts 0.1.1 → 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/NOTICE +15 -0
- package/README.md +14 -18
- 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 +2 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +20 -0
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +7 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/store.d.ts +16 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +775 -2
- 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/dist/x-gts-ref.d.ts +1 -0
- package/dist/x-gts-ref.d.ts.map +1 -1
- package/dist/x-gts-ref.js +65 -0
- package/dist/x-gts-ref.js.map +1 -1
- package/package.json +3 -4
- package/src/gts.ts +40 -5
- package/src/index.ts +22 -0
- package/src/server/server.ts +33 -0
- package/src/server/types.ts +9 -0
- package/src/store.ts +859 -2
- package/src/types.ts +1 -0
- package/src/x-gts-ref.ts +63 -0
- package/tests/gts.test.ts +2 -0
package/dist/store.js
CHANGED
|
@@ -81,8 +81,12 @@ class GtsStore {
|
|
|
81
81
|
}
|
|
82
82
|
validateInstance(gtsId) {
|
|
83
83
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
let objId = gtsId;
|
|
85
|
+
if (gts_1.Gts.isValidGtsID(gtsId)) {
|
|
86
|
+
const gid = gts_1.Gts.parseGtsID(gtsId);
|
|
87
|
+
objId = gid.id;
|
|
88
|
+
}
|
|
89
|
+
const obj = this.get(objId);
|
|
86
90
|
if (!obj) {
|
|
87
91
|
return {
|
|
88
92
|
id: gtsId,
|
|
@@ -174,6 +178,9 @@ class GtsStore {
|
|
|
174
178
|
}
|
|
175
179
|
const normalized = {};
|
|
176
180
|
for (const [key, value] of Object.entries(obj)) {
|
|
181
|
+
// Strip x-gts-ref so Ajv never sees the unknown keyword
|
|
182
|
+
if (key === 'x-gts-ref')
|
|
183
|
+
continue;
|
|
177
184
|
let newKey = key;
|
|
178
185
|
let newValue = value;
|
|
179
186
|
// Convert $$ prefixed keys to $ prefixed keys
|
|
@@ -197,6 +204,23 @@ class GtsStore {
|
|
|
197
204
|
}
|
|
198
205
|
normalized[newKey] = newValue;
|
|
199
206
|
}
|
|
207
|
+
// Clean up combinator arrays: remove subschemas that were x-gts-ref-only (now empty after stripping)
|
|
208
|
+
for (const combinator of ['oneOf', 'anyOf', 'allOf']) {
|
|
209
|
+
if (Array.isArray(normalized[combinator])) {
|
|
210
|
+
normalized[combinator] = normalized[combinator].filter((_sub, idx) => {
|
|
211
|
+
const original = obj[combinator]?.[idx];
|
|
212
|
+
const isXGtsRefOnly = original &&
|
|
213
|
+
typeof original === 'object' &&
|
|
214
|
+
!Array.isArray(original) &&
|
|
215
|
+
Object.keys(original).length === 1 &&
|
|
216
|
+
original['x-gts-ref'] !== undefined;
|
|
217
|
+
return !isXGtsRefOnly;
|
|
218
|
+
});
|
|
219
|
+
if (normalized[combinator].length === 0) {
|
|
220
|
+
delete normalized[combinator];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
200
224
|
// Normalize $id values
|
|
201
225
|
if (normalized['$id'] && typeof normalized['$id'] === 'string') {
|
|
202
226
|
if (normalized['$id'].startsWith(types_1.GTS_URI_PREFIX)) {
|
|
@@ -908,6 +932,755 @@ class GtsStore {
|
|
|
908
932
|
const unique = Array.from(new Set(arr));
|
|
909
933
|
return unique.sort();
|
|
910
934
|
}
|
|
935
|
+
validateSchemaAgainstParent(schemaId) {
|
|
936
|
+
const entity = this.get(schemaId);
|
|
937
|
+
if (!entity) {
|
|
938
|
+
return { id: schemaId, ok: false, error: `Entity not found: ${schemaId}` };
|
|
939
|
+
}
|
|
940
|
+
if (!entity.isSchema) {
|
|
941
|
+
return { id: schemaId, ok: false, error: `Entity is not a schema: ${schemaId}` };
|
|
942
|
+
}
|
|
943
|
+
const content = entity.content;
|
|
944
|
+
// Find parent reference in allOf
|
|
945
|
+
const parentRef = this.findParentRef(content);
|
|
946
|
+
if (!parentRef) {
|
|
947
|
+
// Base schema with no parent → still validate traits
|
|
948
|
+
return this.validateSchemaTraits(schemaId);
|
|
949
|
+
}
|
|
950
|
+
// Resolve parent entity
|
|
951
|
+
const parentId = parentRef.startsWith(types_1.GTS_URI_PREFIX) ? parentRef.substring(types_1.GTS_URI_PREFIX.length) : parentRef;
|
|
952
|
+
const parentEntity = this.get(parentId);
|
|
953
|
+
if (!parentEntity) {
|
|
954
|
+
return { id: schemaId, ok: false, error: `Parent schema not found: ${parentId}` };
|
|
955
|
+
}
|
|
956
|
+
if (!parentEntity.isSchema || !parentEntity.content) {
|
|
957
|
+
return { id: schemaId, ok: false, error: `Parent entity is not a schema: ${parentId}` };
|
|
958
|
+
}
|
|
959
|
+
// Detect cyclic $$ref / $ref references in the schema's own content
|
|
960
|
+
const cycleError = this.detectRefCycle(schemaId, content, new Set([schemaId]));
|
|
961
|
+
if (cycleError) {
|
|
962
|
+
return { id: schemaId, ok: false, error: cycleError };
|
|
963
|
+
}
|
|
964
|
+
// Resolve parent's effective (fully flattened) schema
|
|
965
|
+
const resolvedParent = this.resolveSchemaFully(parentEntity.content);
|
|
966
|
+
// Extract overlay from derived schema (non-$ref subschemas in allOf + top-level)
|
|
967
|
+
const overlay = this.extractOverlay(content);
|
|
968
|
+
// Compare overlay against resolved parent
|
|
969
|
+
const errors = this.compareOverlayToBase(overlay, resolvedParent, '');
|
|
970
|
+
if (errors.length > 0) {
|
|
971
|
+
return { id: schemaId, ok: false, error: errors.join('; ') };
|
|
972
|
+
}
|
|
973
|
+
// OP#13: Validate schema traits across the inheritance chain
|
|
974
|
+
const traitsResult = this.validateSchemaTraits(schemaId);
|
|
975
|
+
if (!traitsResult.ok) {
|
|
976
|
+
return traitsResult;
|
|
977
|
+
}
|
|
978
|
+
return { id: schemaId, ok: true, error: '' };
|
|
979
|
+
}
|
|
980
|
+
// OP#13: Validate schema traits across the inheritance chain
|
|
981
|
+
validateSchemaTraits(schemaId) {
|
|
982
|
+
// Build the chain of schema IDs from base to leaf
|
|
983
|
+
const chain = this.buildSchemaChain(schemaId);
|
|
984
|
+
// Collect trait schemas and trait values from each level, tracking immutability
|
|
985
|
+
const traitSchemas = [];
|
|
986
|
+
const mergedTraits = {};
|
|
987
|
+
const lockedTraits = new Set();
|
|
988
|
+
const knownDefaults = new Map();
|
|
989
|
+
for (const chainSchemaId of chain) {
|
|
990
|
+
const entity = this.get(chainSchemaId);
|
|
991
|
+
if (!entity || !entity.content)
|
|
992
|
+
continue;
|
|
993
|
+
// Collect trait schemas from this level and track which properties this level introduces
|
|
994
|
+
const prevSchemaCount = traitSchemas.length;
|
|
995
|
+
this.collectTraitSchemas(entity.content, traitSchemas);
|
|
996
|
+
const levelSchemaProps = new Set();
|
|
997
|
+
for (const ts of traitSchemas.slice(prevSchemaCount)) {
|
|
998
|
+
if (typeof ts === 'object' && ts !== null && typeof ts.properties === 'object' && ts.properties !== null) {
|
|
999
|
+
for (const [propName, propSchema] of Object.entries(ts.properties)) {
|
|
1000
|
+
levelSchemaProps.add(propName);
|
|
1001
|
+
// Detect default override: ancestor default cannot be changed by descendant
|
|
1002
|
+
if (typeof propSchema === 'object' &&
|
|
1003
|
+
propSchema !== null &&
|
|
1004
|
+
'default' in propSchema) {
|
|
1005
|
+
const newDefault = propSchema.default;
|
|
1006
|
+
if (knownDefaults.has(propName)) {
|
|
1007
|
+
const oldDefault = knownDefaults.get(propName);
|
|
1008
|
+
if (JSON.stringify(oldDefault) !== JSON.stringify(newDefault)) {
|
|
1009
|
+
return {
|
|
1010
|
+
id: schemaId,
|
|
1011
|
+
ok: false,
|
|
1012
|
+
error: `trait schema default for '${propName}' in '${chainSchemaId}' overrides default set by ancestor`,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
knownDefaults.set(propName, newDefault);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
// Collect trait values from this level
|
|
1024
|
+
const levelTraits = {};
|
|
1025
|
+
this.collectTraitValues(entity.content, levelTraits);
|
|
1026
|
+
// Check immutability: trait values set by ancestor are locked unless
|
|
1027
|
+
// this level also introduces a trait schema covering that property
|
|
1028
|
+
for (const [k, v] of Object.entries(levelTraits)) {
|
|
1029
|
+
if (k in mergedTraits && JSON.stringify(mergedTraits[k]) !== JSON.stringify(v) && lockedTraits.has(k)) {
|
|
1030
|
+
return {
|
|
1031
|
+
id: schemaId,
|
|
1032
|
+
ok: false,
|
|
1033
|
+
error: `trait '${k}' in '${chainSchemaId}' overrides value set by ancestor`,
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
// Mark trait values as locked or unlocked based on whether this level
|
|
1038
|
+
// also introduced a trait schema covering the property
|
|
1039
|
+
for (const k of Object.keys(levelTraits)) {
|
|
1040
|
+
if (levelSchemaProps.has(k)) {
|
|
1041
|
+
lockedTraits.delete(k);
|
|
1042
|
+
}
|
|
1043
|
+
else {
|
|
1044
|
+
lockedTraits.add(k);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
Object.assign(mergedTraits, levelTraits);
|
|
1048
|
+
}
|
|
1049
|
+
// If no trait schemas in the chain, nothing to validate
|
|
1050
|
+
if (traitSchemas.length === 0) {
|
|
1051
|
+
if (Object.keys(mergedTraits).length > 0) {
|
|
1052
|
+
return {
|
|
1053
|
+
id: schemaId,
|
|
1054
|
+
ok: false,
|
|
1055
|
+
error: 'x-gts-traits values provided but no x-gts-traits-schema is defined in the inheritance chain',
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
return { id: schemaId, ok: true, error: '' };
|
|
1059
|
+
}
|
|
1060
|
+
// Validate each trait schema
|
|
1061
|
+
for (let i = 0; i < traitSchemas.length; i++) {
|
|
1062
|
+
const ts = traitSchemas[i];
|
|
1063
|
+
// Check: trait schema must have type "object" (or no type, which defaults to object)
|
|
1064
|
+
if (typeof ts === 'object' && ts !== null && ts.type && ts.type !== 'object') {
|
|
1065
|
+
return {
|
|
1066
|
+
id: schemaId,
|
|
1067
|
+
ok: false,
|
|
1068
|
+
error: `x-gts-traits-schema must have type "object", got "${ts.type}"`,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
// Check: trait schema must not contain x-gts-traits
|
|
1072
|
+
if (typeof ts === 'object' && ts !== null && ts['x-gts-traits']) {
|
|
1073
|
+
return {
|
|
1074
|
+
id: schemaId,
|
|
1075
|
+
ok: false,
|
|
1076
|
+
error: 'x-gts-traits-schema must not contain x-gts-traits',
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
// Resolve $ref inside trait schemas and check for cycles
|
|
1081
|
+
const resolvedTraitSchemas = [];
|
|
1082
|
+
for (const ts of traitSchemas) {
|
|
1083
|
+
try {
|
|
1084
|
+
const resolved = this.resolveTraitSchemaRefs(ts, new Set());
|
|
1085
|
+
resolvedTraitSchemas.push(resolved);
|
|
1086
|
+
}
|
|
1087
|
+
catch (e) {
|
|
1088
|
+
return {
|
|
1089
|
+
id: schemaId,
|
|
1090
|
+
ok: false,
|
|
1091
|
+
error: e instanceof Error ? e.message : String(e),
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
// Build effective trait schema (allOf composition)
|
|
1096
|
+
let effectiveSchema;
|
|
1097
|
+
if (resolvedTraitSchemas.length === 1) {
|
|
1098
|
+
effectiveSchema = resolvedTraitSchemas[0];
|
|
1099
|
+
}
|
|
1100
|
+
else {
|
|
1101
|
+
effectiveSchema = {
|
|
1102
|
+
type: 'object',
|
|
1103
|
+
allOf: resolvedTraitSchemas,
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
// Apply defaults from trait schema to merged traits
|
|
1107
|
+
const effectiveTraits = this.applyTraitDefaults(effectiveSchema, mergedTraits);
|
|
1108
|
+
// Validate effective traits against effective schema using AJV
|
|
1109
|
+
try {
|
|
1110
|
+
const normalizedSchema = this.normalizeSchema(effectiveSchema);
|
|
1111
|
+
const validate = this.ajv.compile(normalizedSchema);
|
|
1112
|
+
const isValid = validate(effectiveTraits);
|
|
1113
|
+
if (!isValid) {
|
|
1114
|
+
const errors = validate.errors?.map((e) => `${e.instancePath} ${e.message}`).join('; ') || 'Trait validation failed';
|
|
1115
|
+
return { id: schemaId, ok: false, error: `trait validation: ${errors}` };
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
catch (e) {
|
|
1119
|
+
return {
|
|
1120
|
+
id: schemaId,
|
|
1121
|
+
ok: false,
|
|
1122
|
+
error: `failed to compile trait schema: ${e instanceof Error ? e.message : String(e)}`,
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
// Check for unresolved trait properties (no value and no default)
|
|
1126
|
+
const allProps = this.collectAllTraitProperties(effectiveSchema);
|
|
1127
|
+
for (const [propName, propSchema] of Object.entries(allProps)) {
|
|
1128
|
+
const hasValue = propName in effectiveTraits;
|
|
1129
|
+
const hasDefault = typeof propSchema === 'object' && propSchema !== null && 'default' in propSchema;
|
|
1130
|
+
if (!hasValue && !hasDefault) {
|
|
1131
|
+
return {
|
|
1132
|
+
id: schemaId,
|
|
1133
|
+
ok: false,
|
|
1134
|
+
error: `trait property '${propName}' is not resolved: no value provided and no default defined`,
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return { id: schemaId, ok: true, error: '' };
|
|
1139
|
+
}
|
|
1140
|
+
// OP#13: Entity-level traits validation
|
|
1141
|
+
validateEntityTraits(entityId) {
|
|
1142
|
+
const entity = this.get(entityId);
|
|
1143
|
+
if (!entity) {
|
|
1144
|
+
return { id: entityId, ok: false, error: `Entity not found: ${entityId}` };
|
|
1145
|
+
}
|
|
1146
|
+
if (!entity.isSchema) {
|
|
1147
|
+
return { id: entityId, ok: true, error: '' };
|
|
1148
|
+
}
|
|
1149
|
+
// Build the chain for this schema
|
|
1150
|
+
const chain = this.buildSchemaChain(entityId);
|
|
1151
|
+
const traitSchemas = [];
|
|
1152
|
+
let hasTraitValues = false;
|
|
1153
|
+
for (const chainSchemaId of chain) {
|
|
1154
|
+
const chainEntity = this.get(chainSchemaId);
|
|
1155
|
+
if (!chainEntity || !chainEntity.content)
|
|
1156
|
+
continue;
|
|
1157
|
+
this.collectTraitSchemas(chainEntity.content, traitSchemas);
|
|
1158
|
+
const levelTraits = {};
|
|
1159
|
+
this.collectTraitValues(chainEntity.content, levelTraits);
|
|
1160
|
+
if (Object.keys(levelTraits).length > 0) {
|
|
1161
|
+
hasTraitValues = true;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
if (traitSchemas.length === 0) {
|
|
1165
|
+
return { id: entityId, ok: true, error: '' };
|
|
1166
|
+
}
|
|
1167
|
+
// If trait schemas exist but no trait values, entity is incomplete
|
|
1168
|
+
if (!hasTraitValues) {
|
|
1169
|
+
return {
|
|
1170
|
+
id: entityId,
|
|
1171
|
+
ok: false,
|
|
1172
|
+
error: 'Entity defines x-gts-traits-schema but no x-gts-traits values are provided',
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
// Each trait schema must have additionalProperties: false (closed)
|
|
1176
|
+
for (const ts of traitSchemas) {
|
|
1177
|
+
if (typeof ts === 'object' && ts !== null) {
|
|
1178
|
+
if (ts.additionalProperties !== false) {
|
|
1179
|
+
return {
|
|
1180
|
+
id: entityId,
|
|
1181
|
+
ok: false,
|
|
1182
|
+
error: 'Trait schema must set additionalProperties: false for entity validation',
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return { id: entityId, ok: true, error: '' };
|
|
1188
|
+
}
|
|
1189
|
+
// Build the schema chain from base to leaf for a given schema ID
|
|
1190
|
+
buildSchemaChain(schemaId) {
|
|
1191
|
+
// Parse the schema ID to get segments
|
|
1192
|
+
try {
|
|
1193
|
+
const gtsId = gts_1.Gts.parseGtsID(schemaId);
|
|
1194
|
+
const segments = gtsId.segments;
|
|
1195
|
+
const chain = [];
|
|
1196
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1197
|
+
const id = 'gts.' +
|
|
1198
|
+
segments
|
|
1199
|
+
.slice(0, i + 1)
|
|
1200
|
+
.map((s) => s.segment)
|
|
1201
|
+
.join('');
|
|
1202
|
+
chain.push(id);
|
|
1203
|
+
}
|
|
1204
|
+
return chain;
|
|
1205
|
+
}
|
|
1206
|
+
catch {
|
|
1207
|
+
return [schemaId];
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
// Collect x-gts-traits-schema from a schema content (recursing into allOf)
|
|
1211
|
+
collectTraitSchemas(content, out, depth = 0) {
|
|
1212
|
+
if (depth > 64 || typeof content !== 'object' || content === null)
|
|
1213
|
+
return;
|
|
1214
|
+
if (content['x-gts-traits-schema'] !== undefined) {
|
|
1215
|
+
out.push(content['x-gts-traits-schema']);
|
|
1216
|
+
}
|
|
1217
|
+
if (Array.isArray(content.allOf)) {
|
|
1218
|
+
for (const item of content.allOf) {
|
|
1219
|
+
this.collectTraitSchemas(item, out, depth + 1);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
// Collect x-gts-traits from a schema content (recursing into allOf)
|
|
1224
|
+
collectTraitValues(content, merged, depth = 0) {
|
|
1225
|
+
if (depth > 64 || typeof content !== 'object' || content === null)
|
|
1226
|
+
return;
|
|
1227
|
+
if (typeof content['x-gts-traits'] === 'object' && content['x-gts-traits'] !== null) {
|
|
1228
|
+
Object.assign(merged, content['x-gts-traits']);
|
|
1229
|
+
}
|
|
1230
|
+
if (Array.isArray(content.allOf)) {
|
|
1231
|
+
for (const item of content.allOf) {
|
|
1232
|
+
this.collectTraitValues(item, merged, depth + 1);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
// Resolve $ref inside a trait schema, detecting cycles
|
|
1237
|
+
resolveTraitSchemaRefs(schema, visited, depth = 0) {
|
|
1238
|
+
if (depth > 64)
|
|
1239
|
+
return schema;
|
|
1240
|
+
if (typeof schema !== 'object' || schema === null)
|
|
1241
|
+
return schema;
|
|
1242
|
+
const result = {};
|
|
1243
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
1244
|
+
if (key === '$$ref' || key === '$ref') {
|
|
1245
|
+
const refUri = value;
|
|
1246
|
+
const refId = refUri.startsWith(types_1.GTS_URI_PREFIX) ? refUri.substring(types_1.GTS_URI_PREFIX.length) : refUri;
|
|
1247
|
+
if (visited.has(refId)) {
|
|
1248
|
+
throw new Error(`Cyclic reference detected in trait schema: ${refId}`);
|
|
1249
|
+
}
|
|
1250
|
+
visited.add(refId);
|
|
1251
|
+
const refEntity = this.get(refId);
|
|
1252
|
+
if (!refEntity || !refEntity.content) {
|
|
1253
|
+
throw new Error(`Unresolvable trait schema reference: ${refUri}`);
|
|
1254
|
+
}
|
|
1255
|
+
const resolved = this.resolveTraitSchemaRefs(refEntity.content, visited, depth + 1);
|
|
1256
|
+
// Merge resolved content into result
|
|
1257
|
+
for (const [rk, rv] of Object.entries(resolved)) {
|
|
1258
|
+
if (rk !== '$id' && rk !== '$$id' && rk !== '$schema' && rk !== '$$schema') {
|
|
1259
|
+
result[rk] = rv;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
if (key === 'allOf' && Array.isArray(value)) {
|
|
1265
|
+
result.allOf = value.map((item) => this.resolveTraitSchemaRefs(item, visited, depth + 1));
|
|
1266
|
+
}
|
|
1267
|
+
else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
1268
|
+
result[key] = this.resolveTraitSchemaRefs(value, new Set(visited), depth + 1);
|
|
1269
|
+
}
|
|
1270
|
+
else {
|
|
1271
|
+
result[key] = value;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return result;
|
|
1275
|
+
}
|
|
1276
|
+
// Apply defaults from trait schema to trait values
|
|
1277
|
+
applyTraitDefaults(schema, traits) {
|
|
1278
|
+
const result = { ...traits };
|
|
1279
|
+
const props = this.collectAllTraitProperties(schema);
|
|
1280
|
+
for (const [propName, propSchema] of Object.entries(props)) {
|
|
1281
|
+
if (!(propName in result) && typeof propSchema === 'object' && propSchema !== null && 'default' in propSchema) {
|
|
1282
|
+
result[propName] = propSchema.default;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return result;
|
|
1286
|
+
}
|
|
1287
|
+
// Collect all properties from a trait schema (handling allOf composition)
|
|
1288
|
+
collectAllTraitProperties(schema, depth = 0) {
|
|
1289
|
+
const props = {};
|
|
1290
|
+
if (depth > 64 || typeof schema !== 'object' || schema === null)
|
|
1291
|
+
return props;
|
|
1292
|
+
if (typeof schema.properties === 'object' && schema.properties !== null) {
|
|
1293
|
+
Object.assign(props, schema.properties);
|
|
1294
|
+
}
|
|
1295
|
+
if (Array.isArray(schema.allOf)) {
|
|
1296
|
+
for (const item of schema.allOf) {
|
|
1297
|
+
Object.assign(props, this.collectAllTraitProperties(item, depth + 1));
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return props;
|
|
1301
|
+
}
|
|
1302
|
+
// Detect cyclic $$ref/$ref references reachable from a schema's content
|
|
1303
|
+
detectRefCycle(originId, content, visited, depth = 0) {
|
|
1304
|
+
if (depth > 64 || !content || typeof content !== 'object')
|
|
1305
|
+
return null;
|
|
1306
|
+
// Check direct ref on this object
|
|
1307
|
+
const ref = content['$$ref'] || content['$ref'];
|
|
1308
|
+
if (typeof ref === 'string') {
|
|
1309
|
+
const refId = ref.startsWith(types_1.GTS_URI_PREFIX) ? ref.substring(types_1.GTS_URI_PREFIX.length) : ref;
|
|
1310
|
+
if (visited.has(refId)) {
|
|
1311
|
+
return `Cyclic reference detected: ${refId}`;
|
|
1312
|
+
}
|
|
1313
|
+
const refEntity = this.get(refId);
|
|
1314
|
+
if (refEntity && refEntity.content) {
|
|
1315
|
+
visited.add(refId);
|
|
1316
|
+
const inner = this.detectRefCycle(originId, refEntity.content, visited, depth + 1);
|
|
1317
|
+
if (inner)
|
|
1318
|
+
return inner;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
// Recurse into allOf
|
|
1322
|
+
if (Array.isArray(content.allOf)) {
|
|
1323
|
+
for (const sub of content.allOf) {
|
|
1324
|
+
const inner = this.detectRefCycle(originId, sub, visited, depth + 1);
|
|
1325
|
+
if (inner)
|
|
1326
|
+
return inner;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
findParentRef(schema) {
|
|
1332
|
+
if (!schema || !schema.allOf || !Array.isArray(schema.allOf)) {
|
|
1333
|
+
return null;
|
|
1334
|
+
}
|
|
1335
|
+
for (const sub of schema.allOf) {
|
|
1336
|
+
if (sub && typeof sub === 'object') {
|
|
1337
|
+
const ref = sub['$$ref'] || sub['$ref'];
|
|
1338
|
+
if (typeof ref === 'string') {
|
|
1339
|
+
return ref;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return null;
|
|
1344
|
+
}
|
|
1345
|
+
resolveSchemaFully(schema, visited = new Set()) {
|
|
1346
|
+
const result = {
|
|
1347
|
+
properties: {},
|
|
1348
|
+
required: [],
|
|
1349
|
+
additionalProperties: undefined,
|
|
1350
|
+
type: schema.type,
|
|
1351
|
+
};
|
|
1352
|
+
// If this schema has allOf, resolve each part
|
|
1353
|
+
if (schema.allOf && Array.isArray(schema.allOf)) {
|
|
1354
|
+
for (const sub of schema.allOf) {
|
|
1355
|
+
const ref = sub['$$ref'] || sub['$ref'];
|
|
1356
|
+
if (typeof ref === 'string') {
|
|
1357
|
+
// Resolve referenced schema
|
|
1358
|
+
const refId = ref.startsWith(types_1.GTS_URI_PREFIX) ? ref.substring(types_1.GTS_URI_PREFIX.length) : ref;
|
|
1359
|
+
if (visited.has(refId)) {
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
visited.add(refId);
|
|
1363
|
+
const refEntity = this.get(refId);
|
|
1364
|
+
if (refEntity && refEntity.content) {
|
|
1365
|
+
const resolved = this.resolveSchemaFully(refEntity.content, visited);
|
|
1366
|
+
Object.assign(result.properties, resolved.properties);
|
|
1367
|
+
if (resolved.required) {
|
|
1368
|
+
result.required.push(...resolved.required);
|
|
1369
|
+
}
|
|
1370
|
+
if (resolved.additionalProperties !== undefined) {
|
|
1371
|
+
result.additionalProperties = resolved.additionalProperties;
|
|
1372
|
+
}
|
|
1373
|
+
if (resolved.type && !result.type) {
|
|
1374
|
+
result.type = resolved.type;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
else {
|
|
1379
|
+
// Non-ref subschema - merge it
|
|
1380
|
+
const resolved = this.resolveSchemaFully(sub, visited);
|
|
1381
|
+
// For overlay properties, merge them (they override)
|
|
1382
|
+
for (const [propName, propSchema] of Object.entries(resolved.properties || {})) {
|
|
1383
|
+
if (result.properties[propName]) {
|
|
1384
|
+
// Merge property constraints - overlay tightens base
|
|
1385
|
+
result.properties[propName] = this.mergePropertySchemas(result.properties[propName], propSchema);
|
|
1386
|
+
}
|
|
1387
|
+
else {
|
|
1388
|
+
result.properties[propName] = propSchema;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
if (resolved.required) {
|
|
1392
|
+
result.required.push(...resolved.required);
|
|
1393
|
+
}
|
|
1394
|
+
if (resolved.additionalProperties !== undefined) {
|
|
1395
|
+
result.additionalProperties = resolved.additionalProperties;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
// Add direct properties
|
|
1401
|
+
if (schema.properties) {
|
|
1402
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
1403
|
+
if (result.properties[propName]) {
|
|
1404
|
+
result.properties[propName] = this.mergePropertySchemas(result.properties[propName], propSchema);
|
|
1405
|
+
}
|
|
1406
|
+
else {
|
|
1407
|
+
result.properties[propName] = propSchema;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
// Add direct required
|
|
1412
|
+
if (schema.required && Array.isArray(schema.required)) {
|
|
1413
|
+
result.required.push(...schema.required);
|
|
1414
|
+
}
|
|
1415
|
+
// Direct additionalProperties
|
|
1416
|
+
if (schema.additionalProperties !== undefined) {
|
|
1417
|
+
result.additionalProperties = schema.additionalProperties;
|
|
1418
|
+
}
|
|
1419
|
+
// Deduplicate required
|
|
1420
|
+
result.required = Array.from(new Set(result.required));
|
|
1421
|
+
return result;
|
|
1422
|
+
}
|
|
1423
|
+
mergePropertySchemas(base, overlay) {
|
|
1424
|
+
if (base === false || overlay === false) {
|
|
1425
|
+
return false;
|
|
1426
|
+
}
|
|
1427
|
+
if (typeof base !== 'object' || typeof overlay !== 'object') {
|
|
1428
|
+
return overlay;
|
|
1429
|
+
}
|
|
1430
|
+
const merged = { ...base };
|
|
1431
|
+
for (const [key, val] of Object.entries(overlay)) {
|
|
1432
|
+
if (key === 'properties' && merged.properties) {
|
|
1433
|
+
merged.properties = { ...merged.properties, ...val };
|
|
1434
|
+
}
|
|
1435
|
+
else if (key === 'required' && merged.required) {
|
|
1436
|
+
const mergedReq = new Set([...merged.required, ...val]);
|
|
1437
|
+
merged.required = Array.from(mergedReq);
|
|
1438
|
+
}
|
|
1439
|
+
else {
|
|
1440
|
+
merged[key] = val;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return merged;
|
|
1444
|
+
}
|
|
1445
|
+
extractOverlay(schema) {
|
|
1446
|
+
const overlay = {
|
|
1447
|
+
properties: {},
|
|
1448
|
+
required: [],
|
|
1449
|
+
additionalProperties: undefined,
|
|
1450
|
+
};
|
|
1451
|
+
if (schema.allOf && Array.isArray(schema.allOf)) {
|
|
1452
|
+
for (const sub of schema.allOf) {
|
|
1453
|
+
const ref = sub['$$ref'] || sub['$ref'];
|
|
1454
|
+
if (typeof ref === 'string') {
|
|
1455
|
+
continue; // Skip ref subschemas
|
|
1456
|
+
}
|
|
1457
|
+
// This is a non-ref overlay subschema
|
|
1458
|
+
if (sub.properties) {
|
|
1459
|
+
for (const [propName, propSchema] of Object.entries(sub.properties)) {
|
|
1460
|
+
overlay.properties[propName] = overlay.properties[propName]
|
|
1461
|
+
? this.mergePropertySchemas(overlay.properties[propName], propSchema)
|
|
1462
|
+
: propSchema;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
if (sub.required && Array.isArray(sub.required)) {
|
|
1466
|
+
overlay.required.push(...sub.required);
|
|
1467
|
+
}
|
|
1468
|
+
if (sub.additionalProperties !== undefined) {
|
|
1469
|
+
overlay.additionalProperties = sub.additionalProperties;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
// Add top-level properties (outside allOf)
|
|
1474
|
+
if (schema.properties) {
|
|
1475
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
1476
|
+
overlay.properties[propName] = overlay.properties[propName]
|
|
1477
|
+
? this.mergePropertySchemas(overlay.properties[propName], propSchema)
|
|
1478
|
+
: propSchema;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
if (schema.required && Array.isArray(schema.required)) {
|
|
1482
|
+
overlay.required.push(...schema.required);
|
|
1483
|
+
}
|
|
1484
|
+
if (schema.additionalProperties !== undefined && overlay.additionalProperties === undefined) {
|
|
1485
|
+
overlay.additionalProperties = schema.additionalProperties;
|
|
1486
|
+
}
|
|
1487
|
+
return overlay;
|
|
1488
|
+
}
|
|
1489
|
+
compareOverlayToBase(overlay, baseResolved, path) {
|
|
1490
|
+
const errors = [];
|
|
1491
|
+
const overlayProps = overlay.properties || {};
|
|
1492
|
+
const baseProps = baseResolved.properties || {};
|
|
1493
|
+
for (const [propName, propSchema] of Object.entries(overlayProps)) {
|
|
1494
|
+
const propPath = path ? `${path}.${propName}` : propName;
|
|
1495
|
+
// Property schema set to false
|
|
1496
|
+
if (propSchema === false) {
|
|
1497
|
+
if (baseProps[propName] !== undefined) {
|
|
1498
|
+
errors.push(`Property '${propPath}' is set to false but exists in base`);
|
|
1499
|
+
}
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
const baseProp = baseProps[propName];
|
|
1503
|
+
if (baseProp === undefined || baseProp === null) {
|
|
1504
|
+
// New property not in base
|
|
1505
|
+
if (baseResolved.additionalProperties === false) {
|
|
1506
|
+
errors.push(`Property '${propPath}' not in base and base has additionalProperties: false`);
|
|
1507
|
+
}
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
if (baseProp === false) {
|
|
1511
|
+
// Base already set property to false, overlay can't use it
|
|
1512
|
+
errors.push(`Property '${propPath}' is forbidden in base`);
|
|
1513
|
+
continue;
|
|
1514
|
+
}
|
|
1515
|
+
// Both base and overlay have this property — compare constraints
|
|
1516
|
+
if (typeof propSchema === 'object' && propSchema !== null) {
|
|
1517
|
+
errors.push(...this.comparePropertyConstraints(propSchema, baseProp, propPath));
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
// Check additionalProperties
|
|
1521
|
+
if (baseResolved.additionalProperties === false) {
|
|
1522
|
+
if (overlay.additionalProperties === true) {
|
|
1523
|
+
errors.push('Cannot loosen additionalProperties from false to true');
|
|
1524
|
+
}
|
|
1525
|
+
else if (overlay.additionalProperties === undefined) {
|
|
1526
|
+
errors.push('Base has additionalProperties: false but derived does not restate it');
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
return errors;
|
|
1530
|
+
}
|
|
1531
|
+
comparePropertyConstraints(derived, base, propPath) {
|
|
1532
|
+
const errors = [];
|
|
1533
|
+
if (typeof base !== 'object' || base === null) {
|
|
1534
|
+
return errors;
|
|
1535
|
+
}
|
|
1536
|
+
// Type check
|
|
1537
|
+
const baseType = base.type;
|
|
1538
|
+
const derivedType = derived.type;
|
|
1539
|
+
if (baseType !== undefined && derivedType !== undefined) {
|
|
1540
|
+
if (Array.isArray(derivedType)) {
|
|
1541
|
+
// Derived has array type — widening (fail)
|
|
1542
|
+
if (!Array.isArray(baseType)) {
|
|
1543
|
+
errors.push(`Property '${propPath}' widens type from '${baseType}' to array`);
|
|
1544
|
+
return errors;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
if (Array.isArray(baseType)) {
|
|
1548
|
+
if (!Array.isArray(derivedType)) {
|
|
1549
|
+
// Could be narrowing from array type
|
|
1550
|
+
if (!baseType.includes(derivedType)) {
|
|
1551
|
+
errors.push(`Property '${propPath}' type '${derivedType}' not in base types [${baseType}]`);
|
|
1552
|
+
return errors;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
else if (!Array.isArray(derivedType)) {
|
|
1557
|
+
// Both scalar types
|
|
1558
|
+
if (baseType !== derivedType) {
|
|
1559
|
+
errors.push(`Property '${propPath}' type changed from '${baseType}' to '${derivedType}'`);
|
|
1560
|
+
return errors;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
// Determine if the overlay adds any NEW constraint keywords not in the base.
|
|
1565
|
+
// Under allOf semantics, base constraints are preserved. Drops are only flagged
|
|
1566
|
+
// when the overlay doesn't introduce any new tightening constraints.
|
|
1567
|
+
const CONSTRAINT_KEYWORDS = [
|
|
1568
|
+
'maxLength',
|
|
1569
|
+
'minLength',
|
|
1570
|
+
'maximum',
|
|
1571
|
+
'minimum',
|
|
1572
|
+
'maxItems',
|
|
1573
|
+
'minItems',
|
|
1574
|
+
'enum',
|
|
1575
|
+
'const',
|
|
1576
|
+
'pattern',
|
|
1577
|
+
'items',
|
|
1578
|
+
];
|
|
1579
|
+
const baseConstraintKeys = new Set(CONSTRAINT_KEYWORDS.filter((kw) => base[kw] !== undefined));
|
|
1580
|
+
const derivedConstraintKeys = new Set(CONSTRAINT_KEYWORDS.filter((kw) => derived[kw] !== undefined));
|
|
1581
|
+
const hasNewConstraints = [...derivedConstraintKeys].some((kw) => !baseConstraintKeys.has(kw));
|
|
1582
|
+
// Max constraints (tightening = lower value OK; loosening = higher value FAIL)
|
|
1583
|
+
for (const kw of ['maxLength', 'maximum', 'maxItems']) {
|
|
1584
|
+
if (base[kw] !== undefined) {
|
|
1585
|
+
if (derived[kw] === undefined) {
|
|
1586
|
+
if (!hasNewConstraints) {
|
|
1587
|
+
errors.push(`Property '${propPath}' drops constraint '${kw}'`);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
else if (derived[kw] > base[kw]) {
|
|
1591
|
+
errors.push(`Property '${propPath}' loosens '${kw}' from ${base[kw]} to ${derived[kw]}`);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
// Min constraints (tightening = higher value OK; loosening = lower value FAIL)
|
|
1596
|
+
for (const kw of ['minLength', 'minimum', 'minItems']) {
|
|
1597
|
+
if (base[kw] !== undefined) {
|
|
1598
|
+
if (derived[kw] === undefined) {
|
|
1599
|
+
if (!hasNewConstraints) {
|
|
1600
|
+
errors.push(`Property '${propPath}' drops constraint '${kw}'`);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
else if (derived[kw] < base[kw]) {
|
|
1604
|
+
errors.push(`Property '${propPath}' loosens '${kw}' from ${base[kw]} to ${derived[kw]}`);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
// Enum check
|
|
1609
|
+
if (base.enum !== undefined) {
|
|
1610
|
+
if (derived.enum === undefined) {
|
|
1611
|
+
if (!hasNewConstraints) {
|
|
1612
|
+
errors.push(`Property '${propPath}' drops constraint 'enum'`);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
else {
|
|
1616
|
+
const baseSet = new Set(base.enum.map((v) => JSON.stringify(v)));
|
|
1617
|
+
for (const val of derived.enum) {
|
|
1618
|
+
if (!baseSet.has(JSON.stringify(val))) {
|
|
1619
|
+
errors.push(`Property '${propPath}' enum value '${val}' not in base enum`);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
// Const check
|
|
1625
|
+
if (base.const !== undefined) {
|
|
1626
|
+
if (derived.const === undefined) {
|
|
1627
|
+
if (!hasNewConstraints) {
|
|
1628
|
+
errors.push(`Property '${propPath}' drops constraint 'const'`);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
else if (JSON.stringify(base.const) !== JSON.stringify(derived.const)) {
|
|
1632
|
+
errors.push(`Property '${propPath}' const conflict: ${JSON.stringify(derived.const)} vs base ${JSON.stringify(base.const)}`);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
// Check const in derived against base numeric constraints
|
|
1636
|
+
if (derived.const !== undefined && typeof derived.const === 'number') {
|
|
1637
|
+
if (base.minimum !== undefined && derived.const < base.minimum) {
|
|
1638
|
+
errors.push(`Property '${propPath}' const ${derived.const} violates base minimum ${base.minimum}`);
|
|
1639
|
+
}
|
|
1640
|
+
if (base.maximum !== undefined && derived.const > base.maximum) {
|
|
1641
|
+
errors.push(`Property '${propPath}' const ${derived.const} violates base maximum ${base.maximum}`);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
// Pattern check
|
|
1645
|
+
if (base.pattern !== undefined) {
|
|
1646
|
+
if (derived.pattern === undefined) {
|
|
1647
|
+
if (!hasNewConstraints) {
|
|
1648
|
+
errors.push(`Property '${propPath}' drops constraint 'pattern'`);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
else if (base.pattern !== derived.pattern) {
|
|
1652
|
+
errors.push(`Property '${propPath}' pattern changed from '${base.pattern}' to '${derived.pattern}'`);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
// Items check (array items)
|
|
1656
|
+
if (base.items !== undefined) {
|
|
1657
|
+
if (derived.items === undefined) {
|
|
1658
|
+
if (!hasNewConstraints) {
|
|
1659
|
+
errors.push(`Property '${propPath}' drops constraint 'items'`);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
else if (typeof base.items === 'object' && typeof derived.items === 'object') {
|
|
1663
|
+
errors.push(...this.comparePropertyConstraints(derived.items, base.items, `${propPath}.items`));
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
// Nested object: recursively compare
|
|
1667
|
+
if (base.type === 'object' && derived.type === 'object') {
|
|
1668
|
+
if (base.properties || derived.properties) {
|
|
1669
|
+
const nestedOverlay = {
|
|
1670
|
+
properties: derived.properties || {},
|
|
1671
|
+
required: derived.required || [],
|
|
1672
|
+
additionalProperties: derived.additionalProperties,
|
|
1673
|
+
};
|
|
1674
|
+
const nestedBase = {
|
|
1675
|
+
properties: base.properties || {},
|
|
1676
|
+
required: base.required || [],
|
|
1677
|
+
additionalProperties: base.additionalProperties,
|
|
1678
|
+
};
|
|
1679
|
+
errors.push(...this.compareOverlayToBase(nestedOverlay, nestedBase, propPath));
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
return errors;
|
|
1683
|
+
}
|
|
911
1684
|
getAttribute(gtsId, path) {
|
|
912
1685
|
const entity = this.get(gtsId);
|
|
913
1686
|
if (!entity) {
|