@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.
Files changed (81) hide show
  1. package/.eslintrc.json +16 -0
  2. package/.github/workflows/ci.yml +198 -0
  3. package/.gitmodules +3 -0
  4. package/.prettierrc +7 -0
  5. package/LICENSE +201 -0
  6. package/Makefile +64 -0
  7. package/README.md +298 -0
  8. package/dist/cast.d.ts +9 -0
  9. package/dist/cast.d.ts.map +1 -0
  10. package/dist/cast.js +153 -0
  11. package/dist/cast.js.map +1 -0
  12. package/dist/cli/index.d.ts +3 -0
  13. package/dist/cli/index.d.ts.map +1 -0
  14. package/dist/cli/index.js +318 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/compatibility.d.ts +11 -0
  17. package/dist/compatibility.d.ts.map +1 -0
  18. package/dist/compatibility.js +176 -0
  19. package/dist/compatibility.js.map +1 -0
  20. package/dist/extract.d.ts +13 -0
  21. package/dist/extract.d.ts.map +1 -0
  22. package/dist/extract.js +194 -0
  23. package/dist/extract.js.map +1 -0
  24. package/dist/gts.d.ts +18 -0
  25. package/dist/gts.d.ts.map +1 -0
  26. package/dist/gts.js +472 -0
  27. package/dist/gts.js.map +1 -0
  28. package/dist/index.d.ts +29 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +97 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/query.d.ts +10 -0
  33. package/dist/query.d.ts.map +1 -0
  34. package/dist/query.js +171 -0
  35. package/dist/query.js.map +1 -0
  36. package/dist/relationships.d.ts +7 -0
  37. package/dist/relationships.d.ts.map +1 -0
  38. package/dist/relationships.js +80 -0
  39. package/dist/relationships.js.map +1 -0
  40. package/dist/server/index.d.ts +2 -0
  41. package/dist/server/index.d.ts.map +1 -0
  42. package/dist/server/index.js +132 -0
  43. package/dist/server/index.js.map +1 -0
  44. package/dist/server/server.d.ts +33 -0
  45. package/dist/server/server.d.ts.map +1 -0
  46. package/dist/server/server.js +678 -0
  47. package/dist/server/server.js.map +1 -0
  48. package/dist/server/types.d.ts +61 -0
  49. package/dist/server/types.d.ts.map +1 -0
  50. package/dist/server/types.js +3 -0
  51. package/dist/server/types.js.map +1 -0
  52. package/dist/store.d.ts +39 -0
  53. package/dist/store.d.ts.map +1 -0
  54. package/dist/store.js +1026 -0
  55. package/dist/store.js.map +1 -0
  56. package/dist/types.d.ts +111 -0
  57. package/dist/types.d.ts.map +1 -0
  58. package/dist/types.js +29 -0
  59. package/dist/types.js.map +1 -0
  60. package/dist/x-gts-ref.d.ts +35 -0
  61. package/dist/x-gts-ref.d.ts.map +1 -0
  62. package/dist/x-gts-ref.js +304 -0
  63. package/dist/x-gts-ref.js.map +1 -0
  64. package/jest.config.js +13 -0
  65. package/package.json +54 -0
  66. package/src/cast.ts +179 -0
  67. package/src/cli/index.ts +315 -0
  68. package/src/compatibility.ts +201 -0
  69. package/src/extract.ts +213 -0
  70. package/src/gts.ts +550 -0
  71. package/src/index.ts +97 -0
  72. package/src/query.ts +191 -0
  73. package/src/relationships.ts +91 -0
  74. package/src/server/index.ts +112 -0
  75. package/src/server/server.ts +771 -0
  76. package/src/server/types.ts +74 -0
  77. package/src/store.ts +1178 -0
  78. package/src/types.ts +138 -0
  79. package/src/x-gts-ref.ts +349 -0
  80. package/tests/gts.test.ts +525 -0
  81. package/tsconfig.json +32 -0
package/src/query.ts ADDED
@@ -0,0 +1,191 @@
1
+ import { QueryResult } from './types';
2
+ import { GtsStore } from './store';
3
+ import { Gts } from './gts';
4
+
5
+ export class GtsQuery {
6
+ static query(store: GtsStore, expression: string, limit: number = 100): QueryResult {
7
+ try {
8
+ // Parse the query expression to extract base pattern and filters
9
+ const { basePattern, filters, error } = this.parseQueryExpression(expression);
10
+
11
+ if (error) {
12
+ return {
13
+ query: expression,
14
+ count: 0,
15
+ items: [],
16
+ error: error,
17
+ limit: limit,
18
+ };
19
+ }
20
+
21
+ const results: any[] = [];
22
+
23
+ // Iterate through all entities in the store
24
+ for (const [id, entity] of store['byId']) {
25
+ if (results.length >= limit) break;
26
+
27
+ // Check if ID matches the pattern
28
+ if (!this.matchesIDPattern(id, basePattern)) {
29
+ continue;
30
+ }
31
+
32
+ // Check filters
33
+ if (!this.matchesFilters(entity.content, filters)) {
34
+ continue;
35
+ }
36
+
37
+ results.push(entity.content);
38
+ }
39
+
40
+ return {
41
+ query: expression,
42
+ count: results.length,
43
+ items: results,
44
+ limit: limit,
45
+ };
46
+ } catch (error) {
47
+ return {
48
+ query: expression,
49
+ count: 0,
50
+ items: [],
51
+ error: error instanceof Error ? error.message : String(error),
52
+ limit: limit,
53
+ };
54
+ }
55
+ }
56
+
57
+ private static parseQueryExpression(expr: string): {
58
+ basePattern: string;
59
+ filters: Map<string, string>;
60
+ error?: string;
61
+ } {
62
+ // Split by '[' to separate base pattern from filters
63
+ const parts = expr.split('[');
64
+ const basePattern = parts[0].trim();
65
+ const filters = new Map<string, string>();
66
+
67
+ if (parts.length > 1) {
68
+ // Extract filter string (remove trailing ])
69
+ let filterStr = parts[1].trim();
70
+ if (!filterStr.endsWith(']')) {
71
+ return {
72
+ basePattern,
73
+ filters,
74
+ error: "Invalid query: missing closing bracket ']'",
75
+ };
76
+ }
77
+ filterStr = filterStr.slice(0, -1);
78
+
79
+ // Check if base pattern ends with ~ or ~* (type ID/pattern) - filters not allowed on type queries
80
+ if (basePattern.endsWith('~') || basePattern.endsWith('~*')) {
81
+ return {
82
+ basePattern,
83
+ filters,
84
+ error: 'Invalid query: filters cannot be used with type patterns (ending with ~ or ~*)',
85
+ };
86
+ }
87
+
88
+ // Parse filters
89
+ const filterParts = filterStr.split(',');
90
+ for (const part of filterParts) {
91
+ const trimmed = part.trim();
92
+ if (trimmed.includes('=')) {
93
+ const [key, ...valueParts] = trimmed.split('=');
94
+ const value = valueParts
95
+ .join('=')
96
+ .trim()
97
+ .replace(/^["']|["']$/g, '');
98
+ filters.set(key.trim(), value);
99
+ }
100
+ }
101
+ }
102
+
103
+ // Validate the pattern
104
+ const isWildcard = basePattern.includes('*');
105
+ const validationError = this.validateQueryPattern(basePattern, isWildcard);
106
+ if (validationError) {
107
+ return {
108
+ basePattern,
109
+ filters,
110
+ error: validationError,
111
+ };
112
+ }
113
+
114
+ return { basePattern, filters };
115
+ }
116
+
117
+ private static validateQueryPattern(basePattern: string, isWildcard: boolean): string | undefined {
118
+ if (isWildcard) {
119
+ // Wildcard pattern must end with .* or ~*
120
+ if (!basePattern.endsWith('.*') && !basePattern.endsWith('~*')) {
121
+ return 'Invalid query: wildcard patterns must end with .* or ~*';
122
+ }
123
+
124
+ // Validate as wildcard pattern
125
+ try {
126
+ // Just check it's a valid pattern format
127
+ if (!basePattern.startsWith('gts.')) {
128
+ return "Invalid query: pattern must start with 'gts.'";
129
+ }
130
+ } catch (err) {
131
+ return `Invalid query: ${err}`;
132
+ }
133
+ } else {
134
+ // Non-wildcard pattern must be a complete valid GTS ID
135
+ try {
136
+ const result = Gts.parseID(basePattern);
137
+ if (!result.ok) {
138
+ return `Invalid query: ${result.error}`;
139
+ }
140
+
141
+ // Check if pattern is incomplete (missing version or type)
142
+ // A complete GTS ID must end with a version (v1, v1.2) or ~ for types
143
+ const segments = result.segments || [];
144
+ if (segments.length === 0) {
145
+ return 'Invalid query: GTS ID has no valid segments';
146
+ }
147
+
148
+ const lastSeg = segments[segments.length - 1];
149
+ if (!lastSeg.isType && !lastSeg.verMajor) {
150
+ return 'Invalid query: incomplete GTS ID pattern';
151
+ }
152
+ } catch (err) {
153
+ return `Invalid query: ${err}`;
154
+ }
155
+ }
156
+
157
+ return undefined;
158
+ }
159
+
160
+ private static matchesIDPattern(entityID: string, basePattern: string): boolean {
161
+ // Always use the proper matchIDPattern function which handles wildcards and version matching
162
+ const matchResult = Gts.matchIDPattern(entityID, basePattern);
163
+ return matchResult.match;
164
+ }
165
+
166
+ private static matchesFilters(entityContent: any, filters: Map<string, string>): boolean {
167
+ if (filters.size === 0) {
168
+ return true;
169
+ }
170
+
171
+ if (!entityContent || typeof entityContent !== 'object') {
172
+ return false;
173
+ }
174
+
175
+ for (const [key, value] of filters) {
176
+ const entityValue = String(entityContent[key] ?? '');
177
+
178
+ // Support wildcard in filter values
179
+ if (value === '*') {
180
+ // Wildcard matches any non-empty value
181
+ if (!entityValue || entityValue === 'null' || entityValue === 'undefined') {
182
+ return false;
183
+ }
184
+ } else if (entityValue !== value) {
185
+ return false;
186
+ }
187
+ }
188
+
189
+ return true;
190
+ }
191
+ }
@@ -0,0 +1,91 @@
1
+ import { RelationshipResult } from './types';
2
+ import { GtsStore } from './store';
3
+ import { Gts } from './gts';
4
+
5
+ export class GtsRelationships {
6
+ static resolveRelationships(store: GtsStore, gtsId: string): RelationshipResult {
7
+ try {
8
+ const entity = store.get(gtsId);
9
+ if (!entity) {
10
+ return {
11
+ id: gtsId,
12
+ relationships: [],
13
+ brokenReferences: [],
14
+ error: `Entity not found: ${gtsId}`,
15
+ };
16
+ }
17
+
18
+ const relationships: Set<string> = new Set();
19
+ const brokenReferences: Set<string> = new Set();
20
+
21
+ const visited = new Set<string>();
22
+ this.findRelationships(store, entity.content, relationships, brokenReferences, visited);
23
+
24
+ if (entity.schemaId) {
25
+ relationships.add(entity.schemaId);
26
+ const schemaEntity = store.get(entity.schemaId);
27
+ if (!schemaEntity) {
28
+ brokenReferences.add(entity.schemaId);
29
+ }
30
+ }
31
+
32
+ return {
33
+ id: gtsId,
34
+ relationships: Array.from(relationships).sort(),
35
+ brokenReferences: Array.from(brokenReferences).sort(),
36
+ };
37
+ } catch (error) {
38
+ return {
39
+ id: gtsId,
40
+ relationships: [],
41
+ brokenReferences: [],
42
+ error: error instanceof Error ? error.message : String(error),
43
+ };
44
+ }
45
+ }
46
+
47
+ private static findRelationships(
48
+ store: GtsStore,
49
+ obj: any,
50
+ relationships: Set<string>,
51
+ brokenReferences: Set<string>,
52
+ visited: Set<any>
53
+ ): void {
54
+ if (!obj || typeof obj !== 'object' || visited.has(obj)) {
55
+ return;
56
+ }
57
+
58
+ visited.add(obj);
59
+
60
+ if ('$ref' in obj && typeof obj['$ref'] === 'string') {
61
+ const ref = obj['$ref'];
62
+ const normalized = ref.startsWith('gts://') ? ref.substring(6) : ref;
63
+ if (Gts.isValidGtsID(normalized)) {
64
+ relationships.add(normalized);
65
+ if (!store.get(normalized)) {
66
+ brokenReferences.add(normalized);
67
+ }
68
+ }
69
+ }
70
+
71
+ if ('x-gts-ref' in obj && typeof obj['x-gts-ref'] === 'string') {
72
+ const ref = obj['x-gts-ref'];
73
+ if (Gts.isValidGtsID(ref)) {
74
+ relationships.add(ref);
75
+ if (!store.get(ref)) {
76
+ brokenReferences.add(ref);
77
+ }
78
+ }
79
+ }
80
+
81
+ if (Array.isArray(obj)) {
82
+ for (const item of obj) {
83
+ this.findRelationships(store, item, relationships, brokenReferences, visited);
84
+ }
85
+ } else {
86
+ for (const value of Object.values(obj)) {
87
+ this.findRelationships(store, value, relationships, brokenReferences, visited);
88
+ }
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,112 @@
1
+ import { Command } from 'commander';
2
+ import { GtsServer } from './server';
3
+ import { ServerConfig } from './types';
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import { createJsonEntity } from '../index';
7
+
8
+ const program = new Command();
9
+
10
+ program
11
+ .name('gts-server')
12
+ .description('GTS HTTP Server')
13
+ .version('0.1.0')
14
+ .option('-h, --host <host>', 'Host to bind to', '127.0.0.1')
15
+ .option('-p, --port <port>', 'Port to listen on', '8000')
16
+ .option('-v, --verbose <level>', 'Verbosity level (0=silent, 1=info, 2=debug)', '1')
17
+ .option('--path <path>', 'Path to JSON/schema files to preload')
18
+ .action(async (options) => {
19
+ const config: ServerConfig = {
20
+ host: options.host,
21
+ port: parseInt(options.port, 10),
22
+ verbose: parseInt(options.verbose, 10),
23
+ path: options.path,
24
+ };
25
+
26
+ console.log(`Starting GTS server on http://${config.host}:${config.port}`);
27
+
28
+ const server = new GtsServer(config);
29
+
30
+ // Preload entities from path if provided
31
+ if (config.path) {
32
+ await loadEntitiesFromPath(server, config.path);
33
+ }
34
+
35
+ // Handle graceful shutdown
36
+ process.on('SIGINT', async () => {
37
+ console.log('\nShutting down server...');
38
+ await server.stop();
39
+ process.exit(0);
40
+ });
41
+
42
+ process.on('SIGTERM', async () => {
43
+ console.log('\nShutting down server...');
44
+ await server.stop();
45
+ process.exit(0);
46
+ });
47
+
48
+ // Start the server
49
+ await server.start();
50
+ });
51
+
52
+ async function loadEntitiesFromPath(server: any, dirPath: string): Promise<void> {
53
+ if (!fs.existsSync(dirPath)) {
54
+ console.warn(`Path does not exist: ${dirPath}`);
55
+ return;
56
+ }
57
+
58
+ const stats = fs.statSync(dirPath);
59
+ if (!stats.isDirectory()) {
60
+ console.warn(`Path is not a directory: ${dirPath}`);
61
+ return;
62
+ }
63
+
64
+ console.log(`Loading entities from: ${dirPath}`);
65
+ const files = fs.readdirSync(dirPath);
66
+ let loaded = 0;
67
+ let errors = 0;
68
+
69
+ for (const file of files) {
70
+ if (!file.endsWith('.json')) continue;
71
+
72
+ const filePath = path.join(dirPath, file);
73
+
74
+ try {
75
+ const content = fs.readFileSync(filePath, 'utf-8');
76
+ const data = JSON.parse(content);
77
+
78
+ // Handle arrays of entities
79
+ const entities = Array.isArray(data) ? data : [data];
80
+
81
+ for (const entity of entities) {
82
+ try {
83
+ const gtsEntity = createJsonEntity(entity);
84
+ if (gtsEntity.id) {
85
+ server['store'].register(entity);
86
+ loaded++;
87
+ if (server['config'].verbose >= 2) {
88
+ console.log(` Loaded: ${gtsEntity.id}`);
89
+ }
90
+ }
91
+ } catch (err) {
92
+ errors++;
93
+ if (server['config'].verbose >= 1) {
94
+ console.warn(` Failed to load entity from ${file}: ${err}`);
95
+ }
96
+ }
97
+ }
98
+ } catch (err) {
99
+ errors++;
100
+ if (server['config'].verbose >= 1) {
101
+ console.warn(` Failed to read ${file}: ${err}`);
102
+ }
103
+ }
104
+ }
105
+
106
+ console.log(`Loaded ${loaded} entities (${errors} errors)`);
107
+ }
108
+
109
+ // Run the CLI if this is the main module
110
+ if (require.main === module) {
111
+ program.parse(process.argv);
112
+ }