@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/store.js CHANGED
@@ -81,8 +81,12 @@ class GtsStore {
81
81
  }
82
82
  validateInstance(gtsId) {
83
83
  try {
84
- const gid = gts_1.Gts.parseGtsID(gtsId);
85
- const obj = this.get(gid.id);
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,
@@ -940,8 +944,8 @@ class GtsStore {
940
944
  // Find parent reference in allOf
941
945
  const parentRef = this.findParentRef(content);
942
946
  if (!parentRef) {
943
- // Base schema with no parent → always valid
944
- return { id: schemaId, ok: true, error: '' };
947
+ // Base schema with no parent → still validate traits
948
+ return this.validateSchemaTraits(schemaId);
945
949
  }
946
950
  // Resolve parent entity
947
951
  const parentId = parentRef.startsWith(types_1.GTS_URI_PREFIX) ? parentRef.substring(types_1.GTS_URI_PREFIX.length) : parentRef;
@@ -952,6 +956,11 @@ class GtsStore {
952
956
  if (!parentEntity.isSchema || !parentEntity.content) {
953
957
  return { id: schemaId, ok: false, error: `Parent entity is not a schema: ${parentId}` };
954
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
+ }
955
964
  // Resolve parent's effective (fully flattened) schema
956
965
  const resolvedParent = this.resolveSchemaFully(parentEntity.content);
957
966
  // Extract overlay from derived schema (non-$ref subschemas in allOf + top-level)
@@ -961,8 +970,364 @@ class GtsStore {
961
970
  if (errors.length > 0) {
962
971
  return { id: schemaId, ok: false, error: errors.join('; ') };
963
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
+ }
964
1138
  return { id: schemaId, ok: true, error: '' };
965
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
+ }
966
1331
  findParentRef(schema) {
967
1332
  if (!schema || !schema.allOf || !Array.isArray(schema.allOf)) {
968
1333
  return null;