@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/src/cast.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { CastResult } from './types';
|
|
2
|
+
import { GtsStore } from './store';
|
|
3
|
+
import { Gts } from './gts';
|
|
4
|
+
import { GtsCompatibility } from './compatibility';
|
|
5
|
+
|
|
6
|
+
export class GtsCast {
|
|
7
|
+
static castInstance(store: GtsStore, fromId: string, toSchemaId: string): CastResult {
|
|
8
|
+
try {
|
|
9
|
+
const fromGtsId = Gts.parseGtsID(fromId);
|
|
10
|
+
const fromEntity = store.get(fromGtsId.id);
|
|
11
|
+
|
|
12
|
+
if (!fromEntity) {
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
fromId,
|
|
16
|
+
toId: toSchemaId,
|
|
17
|
+
error: `Instance not found: ${fromId}`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!fromEntity.schemaId) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
fromId,
|
|
25
|
+
toId: toSchemaId,
|
|
26
|
+
error: `No schema found for instance: ${fromId}`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const toGtsId = Gts.parseGtsID(toSchemaId);
|
|
31
|
+
const toSchema = store.get(toGtsId.id);
|
|
32
|
+
|
|
33
|
+
if (!toSchema) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
fromId,
|
|
37
|
+
toId: toSchemaId,
|
|
38
|
+
error: `Target schema not found: ${toSchemaId}`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!toSchema.isSchema) {
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
fromId,
|
|
46
|
+
toId: toSchemaId,
|
|
47
|
+
error: `Target is not a schema: ${toSchemaId}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const fromSchemaEntity = store.get(fromEntity.schemaId);
|
|
52
|
+
if (!fromSchemaEntity) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
fromId,
|
|
56
|
+
toId: toSchemaId,
|
|
57
|
+
error: `Source schema not found: ${fromEntity.schemaId}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const compatCheck = GtsCompatibility.checkCompatibility(store, fromEntity.schemaId, toSchemaId, 'full');
|
|
62
|
+
|
|
63
|
+
if (!compatCheck.compatible) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
fromId,
|
|
67
|
+
toId: toSchemaId,
|
|
68
|
+
error: `Schemas are not compatible: ${compatCheck.errors.join('; ')}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const castedInstance = this.performCast(
|
|
73
|
+
fromEntity.content,
|
|
74
|
+
fromSchemaEntity.content,
|
|
75
|
+
toSchema.content,
|
|
76
|
+
toSchemaId
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
fromId,
|
|
82
|
+
toId: toSchemaId,
|
|
83
|
+
result: castedInstance,
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
fromId,
|
|
89
|
+
toId: toSchemaId,
|
|
90
|
+
error: error instanceof Error ? error.message : String(error),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private static performCast(instance: any, fromSchema: any, toSchema: any, toSchemaId: string): any {
|
|
96
|
+
const result: any = { ...instance };
|
|
97
|
+
|
|
98
|
+
const fromSegments = Gts.parseID(fromSchema['$$id'] || fromSchema['$id']).segments;
|
|
99
|
+
const toSegments = Gts.parseID(toSchemaId).segments;
|
|
100
|
+
|
|
101
|
+
if (fromSegments.length > 0 && toSegments.length > 0) {
|
|
102
|
+
const fromVersion = `v${fromSegments[0].verMajor}.${fromSegments[0].verMinor ?? 0}`;
|
|
103
|
+
const toVersion = `v${toSegments[0].verMajor}.${toSegments[0].verMinor ?? 0}`;
|
|
104
|
+
|
|
105
|
+
if ('gtsId' in result) {
|
|
106
|
+
result.gtsId = result.gtsId.replace(fromVersion, toVersion);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if ('$schema' in result || '$$schema' in result) {
|
|
111
|
+
result['$schema'] = toSchemaId;
|
|
112
|
+
if ('$$schema' in result) {
|
|
113
|
+
result['$$schema'] = toSchemaId;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const toProps = toSchema.properties || {};
|
|
118
|
+
const toRequired = new Set(toSchema.required || []);
|
|
119
|
+
|
|
120
|
+
const filtered: any = {};
|
|
121
|
+
for (const [key, value] of Object.entries(result)) {
|
|
122
|
+
if (key in toProps || key === 'gtsId' || key === '$schema' || key === '$$schema') {
|
|
123
|
+
filtered[key] = value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const prop of toRequired) {
|
|
128
|
+
if (!((prop as string) in filtered)) {
|
|
129
|
+
const propSchema = toProps[prop as string];
|
|
130
|
+
if (propSchema) {
|
|
131
|
+
filtered[prop as string] = this.getDefaultValue(propSchema);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Also add properties with default values that aren't required
|
|
137
|
+
for (const [propName, propSchema] of Object.entries(toProps)) {
|
|
138
|
+
if (!(propName in filtered) && propSchema && typeof propSchema === 'object' && 'default' in propSchema) {
|
|
139
|
+
filtered[propName] = propSchema.default;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return filtered;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private static getDefaultValue(schema: any): any {
|
|
147
|
+
if ('default' in schema) {
|
|
148
|
+
return schema.default;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const type = schema.type;
|
|
152
|
+
if (Array.isArray(type)) {
|
|
153
|
+
if (type.includes('null')) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return this.getDefaultForType(type[0]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return this.getDefaultForType(type);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private static getDefaultForType(type: string): any {
|
|
163
|
+
switch (type) {
|
|
164
|
+
case 'string':
|
|
165
|
+
return '';
|
|
166
|
+
case 'number':
|
|
167
|
+
case 'integer':
|
|
168
|
+
return 0;
|
|
169
|
+
case 'boolean':
|
|
170
|
+
return false;
|
|
171
|
+
case 'array':
|
|
172
|
+
return [];
|
|
173
|
+
case 'object':
|
|
174
|
+
return {};
|
|
175
|
+
default:
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { validateGtsID, parseGtsID, matchIDPattern, idToUUID, extractID, GTS, createJsonEntity } from '../index';
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name('gts')
|
|
11
|
+
.description('GTS CLI - Global Type System command-line interface')
|
|
12
|
+
.version('0.1.0')
|
|
13
|
+
.option('--path <path>', 'Path to JSON and schema files', process.env.GTS_PATH)
|
|
14
|
+
.option('--config <config>', 'Path to GTS config JSON file', process.env.GTS_CONFIG)
|
|
15
|
+
.option('-v, --verbose', 'Verbose output', false);
|
|
16
|
+
|
|
17
|
+
// OP#1 - Validate ID
|
|
18
|
+
program
|
|
19
|
+
.command('validate-id')
|
|
20
|
+
.description('Validate a GTS ID')
|
|
21
|
+
.requiredOption('-i, --id <id>', 'GTS ID to validate')
|
|
22
|
+
.action((options) => {
|
|
23
|
+
const result = validateGtsID(options.id);
|
|
24
|
+
console.log(JSON.stringify(result, null, 2));
|
|
25
|
+
process.exit(result.ok ? 0 : 1);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// OP#2 - Extract ID
|
|
29
|
+
program
|
|
30
|
+
.command('extract-id')
|
|
31
|
+
.description('Extract GTS ID from JSON content')
|
|
32
|
+
.requiredOption('-f, --file <file>', 'JSON file to extract ID from')
|
|
33
|
+
.action((options) => {
|
|
34
|
+
try {
|
|
35
|
+
const content = JSON.parse(fs.readFileSync(options.file, 'utf-8'));
|
|
36
|
+
const result = extractID(content);
|
|
37
|
+
console.log(JSON.stringify(result, null, 2));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(`Error: ${error instanceof Error ? error.message : error}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// OP#3 - Parse ID
|
|
45
|
+
program
|
|
46
|
+
.command('parse-id')
|
|
47
|
+
.description('Parse a GTS ID into components')
|
|
48
|
+
.requiredOption('-i, --id <id>', 'GTS ID to parse')
|
|
49
|
+
.action((options) => {
|
|
50
|
+
const result = parseGtsID(options.id);
|
|
51
|
+
console.log(JSON.stringify(result, null, 2));
|
|
52
|
+
process.exit(result.ok ? 0 : 1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// OP#4 - Match ID
|
|
56
|
+
program
|
|
57
|
+
.command('match-id')
|
|
58
|
+
.description('Match GTS ID against pattern')
|
|
59
|
+
.requiredOption('-p, --pattern <pattern>', 'Pattern to match against')
|
|
60
|
+
.requiredOption('-c, --candidate <candidate>', 'Candidate ID to match')
|
|
61
|
+
.action((options) => {
|
|
62
|
+
const result = matchIDPattern(options.candidate, options.pattern);
|
|
63
|
+
console.log(JSON.stringify(result, null, 2));
|
|
64
|
+
process.exit(result.match ? 0 : 1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// OP#5 - UUID
|
|
68
|
+
program
|
|
69
|
+
.command('uuid')
|
|
70
|
+
.description('Generate UUID from GTS ID')
|
|
71
|
+
.requiredOption('-i, --id <id>', 'GTS ID to convert')
|
|
72
|
+
.action((options) => {
|
|
73
|
+
const result = idToUUID(options.id);
|
|
74
|
+
if (result.error) {
|
|
75
|
+
console.error(`Error: ${result.error}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
console.log(result.uuid);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// OP#6 - Validate Instance
|
|
82
|
+
program
|
|
83
|
+
.command('validate')
|
|
84
|
+
.description('Validate instance against schema')
|
|
85
|
+
.requiredOption('-i, --id <id>', 'Instance ID to validate')
|
|
86
|
+
.action((options, command) => {
|
|
87
|
+
const gts = loadStore(command.parent);
|
|
88
|
+
const result = gts.validateInstance(options.id);
|
|
89
|
+
console.log(JSON.stringify(result, null, 2));
|
|
90
|
+
process.exit(result.ok ? 0 : 1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// OP#7 - Relationships
|
|
94
|
+
program
|
|
95
|
+
.command('relationships')
|
|
96
|
+
.description('Resolve relationships for an entity')
|
|
97
|
+
.requiredOption('-i, --id <id>', 'Entity ID')
|
|
98
|
+
.action((options, command) => {
|
|
99
|
+
const gts = loadStore(command.parent);
|
|
100
|
+
const result = gts.resolveRelationships(options.id);
|
|
101
|
+
console.log(JSON.stringify(result, null, 2));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// OP#8 - Compatibility
|
|
105
|
+
program
|
|
106
|
+
.command('compatibility')
|
|
107
|
+
.description('Check schema compatibility')
|
|
108
|
+
.requiredOption('-o, --old <old>', 'Old schema ID')
|
|
109
|
+
.requiredOption('-n, --new <new>', 'New schema ID')
|
|
110
|
+
.option('-m, --mode <mode>', 'Compatibility mode (backward|forward|full)', 'full')
|
|
111
|
+
.action((options, command) => {
|
|
112
|
+
const gts = loadStore(command.parent);
|
|
113
|
+
const result = gts.checkCompatibility(options.old, options.new, options.mode);
|
|
114
|
+
console.log(JSON.stringify(result, null, 2));
|
|
115
|
+
process.exit(result.compatible ? 0 : 1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// OP#9 - Cast
|
|
119
|
+
program
|
|
120
|
+
.command('cast')
|
|
121
|
+
.description('Cast instance to different schema version')
|
|
122
|
+
.requiredOption('-f, --from <from>', 'Source instance ID')
|
|
123
|
+
.requiredOption('-t, --to <to>', 'Target schema ID')
|
|
124
|
+
.action((options, command) => {
|
|
125
|
+
const gts = loadStore(command.parent);
|
|
126
|
+
const result = gts.castInstance(options.from, options.to);
|
|
127
|
+
console.log(JSON.stringify(result, null, 2));
|
|
128
|
+
process.exit(result.ok ? 0 : 1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// OP#10 - Query
|
|
132
|
+
program
|
|
133
|
+
.command('query')
|
|
134
|
+
.description('Query entities')
|
|
135
|
+
.requiredOption('-e, --expr <expr>', 'Query expression')
|
|
136
|
+
.option('-l, --limit <limit>', 'Maximum results', '100')
|
|
137
|
+
.action((options, command) => {
|
|
138
|
+
const gts = loadStore(command.parent);
|
|
139
|
+
const result = gts.query(options.expr, parseInt(options.limit, 10));
|
|
140
|
+
console.log(JSON.stringify(result, null, 2));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// OP#11 - Attribute
|
|
144
|
+
program
|
|
145
|
+
.command('attr')
|
|
146
|
+
.description('Get attribute value')
|
|
147
|
+
.requiredOption('-p, --path <path>', 'Attribute path (e.g., gts.vendor.pkg.ns.type.v1.0@name)')
|
|
148
|
+
.action((options, command) => {
|
|
149
|
+
const gts = loadStore(command.parent);
|
|
150
|
+
const result = gts.getAttribute(options.path);
|
|
151
|
+
console.log(JSON.stringify(result, null, 2));
|
|
152
|
+
process.exit(result.resolved ? 0 : 1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// List entities
|
|
156
|
+
program
|
|
157
|
+
.command('list')
|
|
158
|
+
.description('List all entities')
|
|
159
|
+
.option('-l, --limit <limit>', 'Maximum results', '100')
|
|
160
|
+
.action((options, command) => {
|
|
161
|
+
const gts = loadStore(command.parent);
|
|
162
|
+
const entities = gts['store'].getAll().slice(0, parseInt(options.limit, 10));
|
|
163
|
+
const result = {
|
|
164
|
+
count: entities.length,
|
|
165
|
+
items: entities.map((e) => e.id),
|
|
166
|
+
};
|
|
167
|
+
console.log(JSON.stringify(result, null, 2));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Server command
|
|
171
|
+
program
|
|
172
|
+
.command('server')
|
|
173
|
+
.description('Start HTTP server')
|
|
174
|
+
.option('--host <host>', 'Host to bind to', '127.0.0.1')
|
|
175
|
+
.option('--port <port>', 'Port to listen on', '8000')
|
|
176
|
+
.action(async (options, command) => {
|
|
177
|
+
const parentOpts = command.parent.opts();
|
|
178
|
+
|
|
179
|
+
// Import server dynamically
|
|
180
|
+
const { GtsServer } = await import('../server/server');
|
|
181
|
+
|
|
182
|
+
const config = {
|
|
183
|
+
host: options.host,
|
|
184
|
+
port: parseInt(options.port, 10),
|
|
185
|
+
verbose: parentOpts.verbose ? 2 : 1,
|
|
186
|
+
path: parentOpts.path,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const server = new GtsServer(config);
|
|
190
|
+
|
|
191
|
+
// Load entities if path provided
|
|
192
|
+
if (config.path) {
|
|
193
|
+
const gts = loadStore(command.parent);
|
|
194
|
+
// Transfer loaded entities to server
|
|
195
|
+
const entities = gts['store'].getAll();
|
|
196
|
+
for (const entity of entities) {
|
|
197
|
+
server['store'].register(entity.content);
|
|
198
|
+
}
|
|
199
|
+
console.log(`Loaded ${entities.length} entities from ${config.path}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await server.start();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// OpenAPI command
|
|
206
|
+
program
|
|
207
|
+
.command('openapi')
|
|
208
|
+
.description('Generate OpenAPI specification')
|
|
209
|
+
.option('-o, --out <file>', 'Output file')
|
|
210
|
+
.action((options) => {
|
|
211
|
+
const spec = {
|
|
212
|
+
openapi: '3.0.0',
|
|
213
|
+
info: {
|
|
214
|
+
title: 'GTS API',
|
|
215
|
+
version: '0.1.0',
|
|
216
|
+
description: 'Global Type System API',
|
|
217
|
+
},
|
|
218
|
+
servers: [
|
|
219
|
+
{
|
|
220
|
+
url: 'http://127.0.0.1:8000',
|
|
221
|
+
description: 'Default GTS Server',
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
paths: {},
|
|
225
|
+
components: {},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const output = JSON.stringify(spec, null, 2);
|
|
229
|
+
|
|
230
|
+
if (options.out) {
|
|
231
|
+
fs.writeFileSync(options.out, output);
|
|
232
|
+
console.log(`OpenAPI spec written to ${options.out}`);
|
|
233
|
+
} else {
|
|
234
|
+
console.log(output);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Helper function to load entities from path
|
|
239
|
+
function loadStore(command: any): GTS {
|
|
240
|
+
const options = command.opts();
|
|
241
|
+
const gts = new GTS({ validateRefs: false });
|
|
242
|
+
|
|
243
|
+
if (!options.path) {
|
|
244
|
+
return gts;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const paths = options.path.split(',');
|
|
248
|
+
|
|
249
|
+
for (const dirPath of paths) {
|
|
250
|
+
if (!fs.existsSync(dirPath)) {
|
|
251
|
+
if (options.verbose) {
|
|
252
|
+
console.warn(`Path does not exist: ${dirPath}`);
|
|
253
|
+
}
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const stats = fs.statSync(dirPath);
|
|
258
|
+
if (!stats.isDirectory()) {
|
|
259
|
+
if (options.verbose) {
|
|
260
|
+
console.warn(`Path is not a directory: ${dirPath}`);
|
|
261
|
+
}
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
loadEntitiesFromDir(gts, dirPath, options.verbose);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return gts;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function loadEntitiesFromDir(gts: GTS, dirPath: string, verbose: boolean): void {
|
|
272
|
+
const files = fs.readdirSync(dirPath);
|
|
273
|
+
|
|
274
|
+
for (const file of files) {
|
|
275
|
+
if (!file.endsWith('.json')) continue;
|
|
276
|
+
|
|
277
|
+
const filePath = path.join(dirPath, file);
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
281
|
+
const data = JSON.parse(content);
|
|
282
|
+
|
|
283
|
+
// Handle arrays of entities
|
|
284
|
+
const entities = Array.isArray(data) ? data : [data];
|
|
285
|
+
|
|
286
|
+
for (const entity of entities) {
|
|
287
|
+
try {
|
|
288
|
+
const gtsEntity = createJsonEntity(entity);
|
|
289
|
+
if (gtsEntity.id) {
|
|
290
|
+
gts.register(entity);
|
|
291
|
+
if (verbose) {
|
|
292
|
+
console.log(`Loaded: ${gtsEntity.id}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch (err) {
|
|
296
|
+
if (verbose) {
|
|
297
|
+
console.warn(`Failed to load entity from ${file}: ${err}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
} catch (err) {
|
|
302
|
+
if (verbose) {
|
|
303
|
+
console.warn(`Failed to read ${file}: ${err}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Parse arguments
|
|
310
|
+
program.parse(process.argv);
|
|
311
|
+
|
|
312
|
+
// Show help if no command provided
|
|
313
|
+
if (!process.argv.slice(2).length) {
|
|
314
|
+
program.outputHelp();
|
|
315
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { CompatibilityResult } from './types';
|
|
2
|
+
import { GtsStore } from './store';
|
|
3
|
+
import { Gts } from './gts';
|
|
4
|
+
|
|
5
|
+
export class GtsCompatibility {
|
|
6
|
+
static checkCompatibility(
|
|
7
|
+
store: GtsStore,
|
|
8
|
+
oldId: string,
|
|
9
|
+
newId: string,
|
|
10
|
+
mode: 'backward' | 'forward' | 'full' = 'full'
|
|
11
|
+
): CompatibilityResult {
|
|
12
|
+
const errors: string[] = [];
|
|
13
|
+
const warnings: string[] = [];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const oldGtsId = Gts.parseGtsID(oldId);
|
|
17
|
+
const newGtsId = Gts.parseGtsID(newId);
|
|
18
|
+
|
|
19
|
+
const oldEntity = store.get(oldGtsId.id);
|
|
20
|
+
const newEntity = store.get(newGtsId.id);
|
|
21
|
+
|
|
22
|
+
if (!oldEntity) {
|
|
23
|
+
errors.push(`Old schema not found: ${oldId}`);
|
|
24
|
+
return { compatible: false, oldId, newId, mode, errors, warnings };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!newEntity) {
|
|
28
|
+
errors.push(`New schema not found: ${newId}`);
|
|
29
|
+
return { compatible: false, oldId, newId, mode, errors, warnings };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!oldEntity.isSchema) {
|
|
33
|
+
errors.push(`Old entity is not a schema: ${oldId}`);
|
|
34
|
+
return { compatible: false, oldId, newId, mode, errors, warnings };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!newEntity.isSchema) {
|
|
38
|
+
errors.push(`New entity is not a schema: ${newId}`);
|
|
39
|
+
return { compatible: false, oldId, newId, mode, errors, warnings };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const oldSchema = oldEntity.content;
|
|
43
|
+
const newSchema = newEntity.content;
|
|
44
|
+
|
|
45
|
+
if (mode === 'backward' || mode === 'full') {
|
|
46
|
+
this.checkBackwardCompatibility(oldSchema, newSchema, errors, warnings);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (mode === 'forward' || mode === 'full') {
|
|
50
|
+
this.checkForwardCompatibility(oldSchema, newSchema, errors, warnings);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
compatible: errors.length === 0,
|
|
55
|
+
oldId,
|
|
56
|
+
newId,
|
|
57
|
+
mode,
|
|
58
|
+
errors,
|
|
59
|
+
warnings,
|
|
60
|
+
};
|
|
61
|
+
} catch (error) {
|
|
62
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
63
|
+
return {
|
|
64
|
+
compatible: false,
|
|
65
|
+
oldId,
|
|
66
|
+
newId,
|
|
67
|
+
mode,
|
|
68
|
+
errors,
|
|
69
|
+
warnings,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private static checkBackwardCompatibility(
|
|
75
|
+
oldSchema: any,
|
|
76
|
+
newSchema: any,
|
|
77
|
+
errors: string[],
|
|
78
|
+
warnings: string[]
|
|
79
|
+
): void {
|
|
80
|
+
const oldProps = oldSchema.properties || {};
|
|
81
|
+
const newProps = newSchema.properties || {};
|
|
82
|
+
const oldRequired = new Set(oldSchema.required || []);
|
|
83
|
+
const newRequired = new Set(newSchema.required || []);
|
|
84
|
+
|
|
85
|
+
for (const prop of oldRequired) {
|
|
86
|
+
if (!newRequired.has(prop)) {
|
|
87
|
+
warnings.push(`Property '${prop}' is no longer required in new schema`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const [propName, propSchema] of Object.entries(oldProps)) {
|
|
92
|
+
if (!(propName in newProps)) {
|
|
93
|
+
if (oldRequired.has(propName)) {
|
|
94
|
+
errors.push(`Required property '${propName}' removed in new schema`);
|
|
95
|
+
} else {
|
|
96
|
+
warnings.push(`Optional property '${propName}' removed in new schema`);
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
this.checkPropertyCompatibility(propName, propSchema, newProps[propName], errors, warnings, 'backward');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private static checkForwardCompatibility(oldSchema: any, newSchema: any, errors: string[], warnings: string[]): void {
|
|
105
|
+
const oldProps = oldSchema.properties || {};
|
|
106
|
+
const newProps = newSchema.properties || {};
|
|
107
|
+
const oldRequired = new Set(oldSchema.required || []);
|
|
108
|
+
const newRequired = new Set(newSchema.required || []);
|
|
109
|
+
|
|
110
|
+
for (const prop of newRequired) {
|
|
111
|
+
if (!oldRequired.has(prop)) {
|
|
112
|
+
errors.push(`New required property '${prop}' added in new schema`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const [propName, propSchema] of Object.entries(newProps)) {
|
|
117
|
+
if (!(propName in oldProps)) {
|
|
118
|
+
if (newRequired.has(propName)) {
|
|
119
|
+
errors.push(`New required property '${propName}' added`);
|
|
120
|
+
} else {
|
|
121
|
+
warnings.push(`New optional property '${propName}' added`);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
this.checkPropertyCompatibility(propName, oldProps[propName], propSchema, errors, warnings, 'forward');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private static checkPropertyCompatibility(
|
|
130
|
+
propName: string,
|
|
131
|
+
oldProp: any,
|
|
132
|
+
newProp: any,
|
|
133
|
+
errors: string[],
|
|
134
|
+
warnings: string[],
|
|
135
|
+
direction: 'backward' | 'forward'
|
|
136
|
+
): void {
|
|
137
|
+
const oldType = this.normalizeType(oldProp.type);
|
|
138
|
+
const newType = this.normalizeType(newProp.type);
|
|
139
|
+
|
|
140
|
+
if (oldType !== newType) {
|
|
141
|
+
if (this.areTypesCompatible(oldType, newType, direction)) {
|
|
142
|
+
warnings.push(`Property '${propName}' type changed from ${oldType} to ${newType}`);
|
|
143
|
+
} else {
|
|
144
|
+
errors.push(`Property '${propName}' type incompatibly changed from ${oldType} to ${newType}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (oldProp.enum && newProp.enum) {
|
|
149
|
+
const oldEnum = new Set(oldProp.enum);
|
|
150
|
+
const newEnum = new Set(newProp.enum);
|
|
151
|
+
|
|
152
|
+
if (direction === 'backward') {
|
|
153
|
+
for (const value of oldEnum) {
|
|
154
|
+
if (!newEnum.has(value)) {
|
|
155
|
+
errors.push(`Enum value '${value}' removed from property '${propName}'`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
for (const value of newEnum) {
|
|
160
|
+
if (!oldEnum.has(value)) {
|
|
161
|
+
warnings.push(`Enum value '${value}' added to property '${propName}'`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private static normalizeType(type: any): string {
|
|
169
|
+
if (Array.isArray(type)) {
|
|
170
|
+
return type.join('|');
|
|
171
|
+
}
|
|
172
|
+
return type || 'any';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private static areTypesCompatible(oldType: string, newType: string, direction: 'backward' | 'forward'): boolean {
|
|
176
|
+
if (oldType === newType) return true;
|
|
177
|
+
|
|
178
|
+
if (direction === 'backward') {
|
|
179
|
+
if (newType === 'any') return true;
|
|
180
|
+
if (oldType === 'integer' && newType === 'number') return true;
|
|
181
|
+
} else {
|
|
182
|
+
if (oldType === 'any') return true;
|
|
183
|
+
if (newType === 'integer' && oldType === 'number') return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const oldTypes = new Set(oldType.split('|'));
|
|
187
|
+
const newTypes = new Set(newType.split('|'));
|
|
188
|
+
|
|
189
|
+
if (direction === 'backward') {
|
|
190
|
+
for (const t of oldTypes) {
|
|
191
|
+
if (!newTypes.has(t)) return false;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
for (const t of newTypes) {
|
|
195
|
+
if (!oldTypes.has(t)) return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
}
|