@jackchuka/gql-ingest 1.0.2 → 1.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/bin/cli.js +48 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.test.d.ts +2 -0
- package/dist/config.test.d.ts.map +1 -0
- package/dist/dependency-resolver.d.ts +17 -0
- package/dist/dependency-resolver.d.ts.map +1 -0
- package/dist/dependency-resolver.test.d.ts +2 -0
- package/dist/dependency-resolver.test.d.ts.map +1 -0
- package/dist/graphql-client.d.ts +4 -1
- package/dist/graphql-client.d.ts.map +1 -1
- package/dist/mapper.d.ts +11 -3
- package/dist/mapper.d.ts.map +1 -1
- package/dist/metrics.d.ts +33 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.test.d.ts +2 -0
- package/dist/metrics.test.d.ts.map +1 -0
- package/package.json +5 -2
- package/src/cli.ts +113 -9
- package/src/config.test.ts +205 -0
- package/src/config.ts +92 -0
- package/src/dependency-resolver.test.ts +197 -0
- package/src/dependency-resolver.ts +98 -0
- package/src/graphql-client.ts +28 -2
- package/src/mapper.test.ts +81 -2
- package/src/mapper.ts +169 -31
- package/src/metrics.test.ts +207 -0
- package/src/metrics.ts +131 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { loadConfig, getEntityConfig, DEFAULT_CONFIG } from "./config";
|
|
4
|
+
|
|
5
|
+
jest.mock("fs");
|
|
6
|
+
const mockFs = fs as jest.Mocked<typeof fs>;
|
|
7
|
+
|
|
8
|
+
describe("Configuration", () => {
|
|
9
|
+
const testConfigDir = "/test/config";
|
|
10
|
+
const configPath = path.join(testConfigDir, "config.yaml");
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
jest.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("loadConfig", () => {
|
|
17
|
+
it("should return default config when no config.yaml exists", () => {
|
|
18
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
19
|
+
const consoleSpy = jest.spyOn(console, "log").mockImplementation();
|
|
20
|
+
|
|
21
|
+
const config = loadConfig(testConfigDir);
|
|
22
|
+
|
|
23
|
+
expect(config).toEqual(DEFAULT_CONFIG);
|
|
24
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
25
|
+
"No config.yaml found, using default sequential processing"
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
consoleSpy.mockRestore();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should load and merge YAML configuration", () => {
|
|
32
|
+
const yamlContent = `
|
|
33
|
+
parallelProcessing:
|
|
34
|
+
concurrency: 5
|
|
35
|
+
enableEntityParallelization: true
|
|
36
|
+
|
|
37
|
+
entityConfig:
|
|
38
|
+
users:
|
|
39
|
+
concurrency: 2
|
|
40
|
+
preserveRowOrder: true
|
|
41
|
+
|
|
42
|
+
entityDependencies:
|
|
43
|
+
products: ["users"]
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
47
|
+
mockFs.readFileSync.mockReturnValue(yamlContent);
|
|
48
|
+
|
|
49
|
+
const config = loadConfig(testConfigDir);
|
|
50
|
+
|
|
51
|
+
expect(config.parallelProcessing.concurrency).toBe(5);
|
|
52
|
+
expect(config.parallelProcessing.enableEntityParallelization).toBe(true);
|
|
53
|
+
expect(config.entityConfig.users.concurrency).toBe(2);
|
|
54
|
+
expect(config.entityConfig.users.preserveRowOrder).toBe(true);
|
|
55
|
+
expect(config.entityDependencies.products).toEqual(["users"]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should merge partial configuration with defaults", () => {
|
|
59
|
+
const yamlContent = `
|
|
60
|
+
parallelProcessing:
|
|
61
|
+
concurrency: 10
|
|
62
|
+
entityConfig:
|
|
63
|
+
products:
|
|
64
|
+
concurrency: 20
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
68
|
+
mockFs.readFileSync.mockReturnValue(yamlContent);
|
|
69
|
+
|
|
70
|
+
const config = loadConfig(testConfigDir);
|
|
71
|
+
|
|
72
|
+
// Should merge with defaults
|
|
73
|
+
expect(config.parallelProcessing.concurrency).toBe(10);
|
|
74
|
+
expect(config.parallelProcessing.enableEntityParallelization).toBe(false); // default
|
|
75
|
+
expect(config.parallelProcessing.preserveRowOrder).toBe(true); // default
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should handle invalid YAML gracefully", () => {
|
|
79
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
80
|
+
mockFs.readFileSync.mockReturnValue("invalid: yaml: content: [");
|
|
81
|
+
|
|
82
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
83
|
+
|
|
84
|
+
const config = loadConfig(testConfigDir);
|
|
85
|
+
|
|
86
|
+
expect(config).toEqual(DEFAULT_CONFIG);
|
|
87
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
88
|
+
expect.stringContaining("Warning: Failed to parse config.yaml")
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
consoleSpy.mockRestore();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should handle file read errors gracefully", () => {
|
|
95
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
96
|
+
mockFs.readFileSync.mockImplementation(() => {
|
|
97
|
+
throw new Error("File read error");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
101
|
+
|
|
102
|
+
const config = loadConfig(testConfigDir);
|
|
103
|
+
|
|
104
|
+
expect(config).toEqual(DEFAULT_CONFIG);
|
|
105
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
106
|
+
expect.stringContaining("Warning: Failed to parse config.yaml")
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
consoleSpy.mockRestore();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("getEntityConfig", () => {
|
|
114
|
+
const globalConfig = {
|
|
115
|
+
parallelProcessing: {
|
|
116
|
+
concurrency: 10,
|
|
117
|
+
enableEntityParallelization: true,
|
|
118
|
+
preserveRowOrder: false,
|
|
119
|
+
preserveEntityOrder: false,
|
|
120
|
+
},
|
|
121
|
+
entityConfig: {
|
|
122
|
+
users: {
|
|
123
|
+
concurrency: 2,
|
|
124
|
+
preserveRowOrder: true,
|
|
125
|
+
},
|
|
126
|
+
products: {
|
|
127
|
+
concurrency: 20,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
entityDependencies: {},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
it("should return global config for entity without overrides", () => {
|
|
134
|
+
const entityConfig = getEntityConfig("orders", globalConfig);
|
|
135
|
+
|
|
136
|
+
expect(entityConfig).toEqual(globalConfig.parallelProcessing);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should merge entity overrides with global config", () => {
|
|
140
|
+
const entityConfig = getEntityConfig("products", globalConfig);
|
|
141
|
+
|
|
142
|
+
expect(entityConfig.concurrency).toBe(20); // overridden
|
|
143
|
+
expect(entityConfig.enableEntityParallelization).toBe(true); // from global
|
|
144
|
+
expect(entityConfig.preserveRowOrder).toBe(false); // from global
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should apply preserveRowOrder constraint", () => {
|
|
148
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
149
|
+
|
|
150
|
+
const entityConfig = getEntityConfig("users", globalConfig);
|
|
151
|
+
|
|
152
|
+
expect(entityConfig.concurrency).toBe(1); // forced to 1
|
|
153
|
+
expect(entityConfig.preserveRowOrder).toBe(true);
|
|
154
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
155
|
+
"Entity 'users': preserveRowOrder=true forces concurrency=1 (was 2)"
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
consoleSpy.mockRestore();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should not apply constraint when concurrency is already 1", () => {
|
|
162
|
+
const config = {
|
|
163
|
+
...globalConfig,
|
|
164
|
+
entityConfig: {
|
|
165
|
+
...globalConfig.entityConfig,
|
|
166
|
+
sequential: {
|
|
167
|
+
concurrency: 1,
|
|
168
|
+
preserveRowOrder: true,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
174
|
+
|
|
175
|
+
const entityConfig = getEntityConfig("sequential", config);
|
|
176
|
+
|
|
177
|
+
expect(entityConfig.concurrency).toBe(1);
|
|
178
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
179
|
+
|
|
180
|
+
consoleSpy.mockRestore();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should not apply constraint when preserveRowOrder is false", () => {
|
|
184
|
+
const config = {
|
|
185
|
+
...globalConfig,
|
|
186
|
+
entityConfig: {
|
|
187
|
+
...globalConfig.entityConfig,
|
|
188
|
+
bulk: {
|
|
189
|
+
concurrency: 50,
|
|
190
|
+
preserveRowOrder: false,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
196
|
+
|
|
197
|
+
const entityConfig = getEntityConfig("bulk", config);
|
|
198
|
+
|
|
199
|
+
expect(entityConfig.concurrency).toBe(50);
|
|
200
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
201
|
+
|
|
202
|
+
consoleSpy.mockRestore();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import * as yaml from "js-yaml";
|
|
4
|
+
|
|
5
|
+
export interface ParallelProcessingConfig {
|
|
6
|
+
concurrency: number;
|
|
7
|
+
enableEntityParallelization: boolean;
|
|
8
|
+
preserveRowOrder: boolean;
|
|
9
|
+
preserveEntityOrder: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface EntityConfig {
|
|
13
|
+
concurrency?: number;
|
|
14
|
+
preserveRowOrder?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ProcessingConfig {
|
|
18
|
+
parallelProcessing: ParallelProcessingConfig;
|
|
19
|
+
entityConfig: Record<string, EntityConfig>;
|
|
20
|
+
entityDependencies: Record<string, string[]>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FullConfig extends ProcessingConfig {
|
|
24
|
+
// Future: additional config sections can be added here
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_PARALLEL_CONFIG: ParallelProcessingConfig = {
|
|
28
|
+
concurrency: 1,
|
|
29
|
+
enableEntityParallelization: false,
|
|
30
|
+
preserveRowOrder: true,
|
|
31
|
+
preserveEntityOrder: true,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const DEFAULT_CONFIG: ProcessingConfig = {
|
|
35
|
+
parallelProcessing: DEFAULT_PARALLEL_CONFIG,
|
|
36
|
+
entityConfig: {},
|
|
37
|
+
entityDependencies: {},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function loadConfig(configDir: string): ProcessingConfig {
|
|
41
|
+
const configPath = path.join(configDir, "config.yaml");
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
if (!fs.existsSync(configPath)) {
|
|
45
|
+
console.log("No config.yaml found, using default sequential processing");
|
|
46
|
+
return DEFAULT_CONFIG;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fileContents = fs.readFileSync(configPath, "utf8");
|
|
50
|
+
const yamlConfig = yaml.load(fileContents) as Partial<FullConfig>;
|
|
51
|
+
|
|
52
|
+
return mergeWithDefaults(yamlConfig);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.warn(
|
|
55
|
+
`Warning: Failed to parse config.yaml: ${error}. Using defaults.`
|
|
56
|
+
);
|
|
57
|
+
return DEFAULT_CONFIG;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function mergeWithDefaults(yamlConfig: Partial<FullConfig>): ProcessingConfig {
|
|
62
|
+
return {
|
|
63
|
+
parallelProcessing: {
|
|
64
|
+
...DEFAULT_PARALLEL_CONFIG,
|
|
65
|
+
...(yamlConfig.parallelProcessing || {}),
|
|
66
|
+
},
|
|
67
|
+
entityConfig: yamlConfig.entityConfig || {},
|
|
68
|
+
entityDependencies: yamlConfig.entityDependencies || {},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getEntityConfig(
|
|
73
|
+
entityName: string,
|
|
74
|
+
globalConfig: ProcessingConfig
|
|
75
|
+
): ParallelProcessingConfig {
|
|
76
|
+
const entityOverrides = globalConfig.entityConfig[entityName] || {};
|
|
77
|
+
|
|
78
|
+
let finalConfig = {
|
|
79
|
+
...globalConfig.parallelProcessing,
|
|
80
|
+
...entityOverrides,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Apply constraint: preserveRowOrder forces concurrency = 1
|
|
84
|
+
if (finalConfig.preserveRowOrder && finalConfig.concurrency > 1) {
|
|
85
|
+
console.warn(
|
|
86
|
+
`Entity '${entityName}': preserveRowOrder=true forces concurrency=1 (was ${finalConfig.concurrency})`
|
|
87
|
+
);
|
|
88
|
+
finalConfig.concurrency = 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return finalConfig;
|
|
92
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { DependencyResolver } from "./dependency-resolver";
|
|
2
|
+
|
|
3
|
+
describe("DependencyResolver", () => {
|
|
4
|
+
describe("resolveExecutionOrder", () => {
|
|
5
|
+
it("should handle entities with no dependencies", () => {
|
|
6
|
+
const entities = ["users", "products", "orders"];
|
|
7
|
+
const dependencies = {};
|
|
8
|
+
const resolver = new DependencyResolver(entities, dependencies);
|
|
9
|
+
|
|
10
|
+
const waves = resolver.resolveExecutionOrder();
|
|
11
|
+
|
|
12
|
+
expect(waves).toHaveLength(1);
|
|
13
|
+
expect(waves[0].wave).toBe(0);
|
|
14
|
+
expect(waves[0].entities).toEqual(["users", "products", "orders"]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should resolve simple linear dependencies", () => {
|
|
18
|
+
const entities = ["users", "products", "orders"];
|
|
19
|
+
const dependencies = {
|
|
20
|
+
products: ["users"],
|
|
21
|
+
orders: ["products"],
|
|
22
|
+
};
|
|
23
|
+
const resolver = new DependencyResolver(entities, dependencies);
|
|
24
|
+
|
|
25
|
+
const waves = resolver.resolveExecutionOrder();
|
|
26
|
+
|
|
27
|
+
expect(waves).toHaveLength(3);
|
|
28
|
+
expect(waves[0].entities).toEqual(["users"]);
|
|
29
|
+
expect(waves[1].entities).toEqual(["products"]);
|
|
30
|
+
expect(waves[2].entities).toEqual(["orders"]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should resolve complex dependency graph", () => {
|
|
34
|
+
const entities = ["users", "categories", "products", "orders", "reviews"];
|
|
35
|
+
const dependencies = {
|
|
36
|
+
products: ["users", "categories"],
|
|
37
|
+
orders: ["users", "products"],
|
|
38
|
+
reviews: ["users", "products"],
|
|
39
|
+
};
|
|
40
|
+
const resolver = new DependencyResolver(entities, dependencies);
|
|
41
|
+
|
|
42
|
+
const waves = resolver.resolveExecutionOrder();
|
|
43
|
+
|
|
44
|
+
expect(waves).toHaveLength(3);
|
|
45
|
+
|
|
46
|
+
// Wave 0: users and categories (no dependencies)
|
|
47
|
+
expect(waves[0].entities.sort()).toEqual(["categories", "users"]);
|
|
48
|
+
|
|
49
|
+
// Wave 1: products (depends on users and categories)
|
|
50
|
+
expect(waves[1].entities).toEqual(["products"]);
|
|
51
|
+
|
|
52
|
+
// Wave 2: orders and reviews (both depend on users and products)
|
|
53
|
+
expect(waves[2].entities.sort()).toEqual(["orders", "reviews"]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should handle entities with multiple dependencies", () => {
|
|
57
|
+
const entities = ["a", "b", "c", "d"];
|
|
58
|
+
const dependencies = {
|
|
59
|
+
c: ["a", "b"],
|
|
60
|
+
d: ["a", "b"],
|
|
61
|
+
};
|
|
62
|
+
const resolver = new DependencyResolver(entities, dependencies);
|
|
63
|
+
|
|
64
|
+
const waves = resolver.resolveExecutionOrder();
|
|
65
|
+
|
|
66
|
+
expect(waves).toHaveLength(2);
|
|
67
|
+
expect(waves[0].entities.sort()).toEqual(["a", "b"]);
|
|
68
|
+
expect(waves[1].entities.sort()).toEqual(["c", "d"]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should detect circular dependencies", () => {
|
|
72
|
+
const entities = ["a", "b", "c"];
|
|
73
|
+
const dependencies = {
|
|
74
|
+
a: ["b"],
|
|
75
|
+
b: ["c"],
|
|
76
|
+
c: ["a"], // circular
|
|
77
|
+
};
|
|
78
|
+
const resolver = new DependencyResolver(entities, dependencies);
|
|
79
|
+
|
|
80
|
+
expect(() => resolver.resolveExecutionOrder()).toThrow(
|
|
81
|
+
"Circular dependency detected or missing dependencies for entities: a, b, c"
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should detect missing dependencies", () => {
|
|
86
|
+
const entities = ["a", "b"];
|
|
87
|
+
const dependencies = {
|
|
88
|
+
a: ["missing"],
|
|
89
|
+
b: ["a"],
|
|
90
|
+
};
|
|
91
|
+
const resolver = new DependencyResolver(entities, dependencies);
|
|
92
|
+
|
|
93
|
+
expect(() => resolver.resolveExecutionOrder()).toThrow(
|
|
94
|
+
"Circular dependency detected or missing dependencies for entities: a, b"
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("validateDependencies", () => {
|
|
100
|
+
it("should return no errors for valid dependencies", () => {
|
|
101
|
+
const entities = ["users", "products", "orders"];
|
|
102
|
+
const dependencies = {
|
|
103
|
+
products: ["users"],
|
|
104
|
+
orders: ["users", "products"],
|
|
105
|
+
};
|
|
106
|
+
const resolver = new DependencyResolver(entities, dependencies);
|
|
107
|
+
|
|
108
|
+
const errors = resolver.validateDependencies();
|
|
109
|
+
|
|
110
|
+
expect(errors).toHaveLength(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should detect entity with dependencies not in entity list", () => {
|
|
114
|
+
const entities = ["users", "products"];
|
|
115
|
+
const dependencies = {
|
|
116
|
+
products: ["users"],
|
|
117
|
+
orders: ["products"], // orders not in entities list
|
|
118
|
+
};
|
|
119
|
+
const resolver = new DependencyResolver(entities, dependencies);
|
|
120
|
+
|
|
121
|
+
const errors = resolver.validateDependencies();
|
|
122
|
+
|
|
123
|
+
expect(errors).toContain(
|
|
124
|
+
"Entity 'orders' has dependencies but is not in the entity list"
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should detect dependencies on non-existent entities", () => {
|
|
129
|
+
const entities = ["users", "products"];
|
|
130
|
+
const dependencies = {
|
|
131
|
+
products: ["users", "categories"], // categories doesn't exist
|
|
132
|
+
};
|
|
133
|
+
const resolver = new DependencyResolver(entities, dependencies);
|
|
134
|
+
|
|
135
|
+
const errors = resolver.validateDependencies();
|
|
136
|
+
|
|
137
|
+
expect(errors).toContain(
|
|
138
|
+
"Entity 'products' depends on 'categories' which does not exist"
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should detect multiple validation errors", () => {
|
|
143
|
+
const entities = ["users"];
|
|
144
|
+
const dependencies = {
|
|
145
|
+
products: ["categories"], // products not in list, categories doesn't exist
|
|
146
|
+
orders: ["missing"], // orders not in list, missing doesn't exist
|
|
147
|
+
};
|
|
148
|
+
const resolver = new DependencyResolver(entities, dependencies);
|
|
149
|
+
|
|
150
|
+
const errors = resolver.validateDependencies();
|
|
151
|
+
|
|
152
|
+
expect(errors).toHaveLength(2);
|
|
153
|
+
expect(errors).toContain(
|
|
154
|
+
"Entity 'products' has dependencies but is not in the entity list"
|
|
155
|
+
);
|
|
156
|
+
expect(errors).toContain(
|
|
157
|
+
"Entity 'orders' has dependencies but is not in the entity list"
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("getDependents", () => {
|
|
163
|
+
it("should return entities that depend on the given entity", () => {
|
|
164
|
+
const entities = ["users", "products", "orders", "reviews"];
|
|
165
|
+
const dependencies = {
|
|
166
|
+
products: ["users"],
|
|
167
|
+
orders: ["users", "products"],
|
|
168
|
+
reviews: ["users", "products"],
|
|
169
|
+
};
|
|
170
|
+
const resolver = new DependencyResolver(entities, dependencies);
|
|
171
|
+
|
|
172
|
+
const usersDependents = resolver.getDependents("users");
|
|
173
|
+
const productsDependents = resolver.getDependents("products");
|
|
174
|
+
const ordersDependents = resolver.getDependents("orders");
|
|
175
|
+
|
|
176
|
+
expect(usersDependents.sort()).toEqual(["orders", "products", "reviews"]);
|
|
177
|
+
expect(productsDependents.sort()).toEqual(["orders", "reviews"]);
|
|
178
|
+
expect(ordersDependents).toEqual([]);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("getDependencies", () => {
|
|
183
|
+
it("should return dependencies for the given entity", () => {
|
|
184
|
+
const entities = ["users", "products", "orders"];
|
|
185
|
+
const dependencies = {
|
|
186
|
+
products: ["users"],
|
|
187
|
+
orders: ["users", "products"],
|
|
188
|
+
};
|
|
189
|
+
const resolver = new DependencyResolver(entities, dependencies);
|
|
190
|
+
|
|
191
|
+
expect(resolver.getDependencies("users")).toEqual([]);
|
|
192
|
+
expect(resolver.getDependencies("products")).toEqual(["users"]);
|
|
193
|
+
expect(resolver.getDependencies("orders")).toEqual(["users", "products"]);
|
|
194
|
+
expect(resolver.getDependencies("nonexistent")).toEqual([]);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export interface DependencyGraph {
|
|
2
|
+
[entityName: string]: string[];
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface ExecutionWave {
|
|
6
|
+
entities: string[];
|
|
7
|
+
wave: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class DependencyResolver {
|
|
11
|
+
private dependencies: DependencyGraph;
|
|
12
|
+
private entities: string[];
|
|
13
|
+
|
|
14
|
+
constructor(entities: string[], dependencies: DependencyGraph = {}) {
|
|
15
|
+
this.entities = entities;
|
|
16
|
+
this.dependencies = dependencies;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
resolveExecutionOrder(): ExecutionWave[] {
|
|
20
|
+
const waves: ExecutionWave[] = [];
|
|
21
|
+
const processed = new Set<string>();
|
|
22
|
+
const inProgress = new Set<string>();
|
|
23
|
+
let waveNumber = 0;
|
|
24
|
+
|
|
25
|
+
while (processed.size < this.entities.length) {
|
|
26
|
+
const currentWave: string[] = [];
|
|
27
|
+
|
|
28
|
+
for (const entity of this.entities) {
|
|
29
|
+
if (processed.has(entity) || inProgress.has(entity)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const deps = this.dependencies[entity] || [];
|
|
34
|
+
const canProcess = deps.every((dep) => processed.has(dep));
|
|
35
|
+
|
|
36
|
+
if (canProcess) {
|
|
37
|
+
currentWave.push(entity);
|
|
38
|
+
inProgress.add(entity);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (currentWave.length === 0) {
|
|
43
|
+
const remaining = this.entities.filter((e) => !processed.has(e));
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Circular dependency detected or missing dependencies for entities: ${remaining.join(
|
|
46
|
+
", "
|
|
47
|
+
)}`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
waves.push({
|
|
52
|
+
entities: currentWave,
|
|
53
|
+
wave: waveNumber++,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
currentWave.forEach((entity) => {
|
|
57
|
+
processed.add(entity);
|
|
58
|
+
inProgress.delete(entity);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return waves;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
validateDependencies(): string[] {
|
|
66
|
+
const errors: string[] = [];
|
|
67
|
+
const entitySet = new Set(this.entities);
|
|
68
|
+
|
|
69
|
+
for (const [entity, deps] of Object.entries(this.dependencies)) {
|
|
70
|
+
if (!entitySet.has(entity)) {
|
|
71
|
+
errors.push(
|
|
72
|
+
`Entity '${entity}' has dependencies but is not in the entity list`
|
|
73
|
+
);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const dep of deps) {
|
|
78
|
+
if (!entitySet.has(dep)) {
|
|
79
|
+
errors.push(
|
|
80
|
+
`Entity '${entity}' depends on '${dep}' which does not exist`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return errors;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getDependents(entityName: string): string[] {
|
|
90
|
+
return Object.entries(this.dependencies)
|
|
91
|
+
.filter(([_, deps]) => deps.includes(entityName))
|
|
92
|
+
.map(([entity, _]) => entity);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getDependencies(entityName: string): string[] {
|
|
96
|
+
return this.dependencies[entityName] || [];
|
|
97
|
+
}
|
|
98
|
+
}
|
package/src/graphql-client.ts
CHANGED
|
@@ -1,20 +1,46 @@
|
|
|
1
1
|
import { GraphQLClient } from 'graphql-request';
|
|
2
|
+
import { MetricsCollector } from './metrics';
|
|
2
3
|
|
|
3
4
|
export class GraphQLClientWrapper {
|
|
4
5
|
private client: GraphQLClient;
|
|
6
|
+
private metrics?: MetricsCollector;
|
|
7
|
+
private verbose: boolean;
|
|
5
8
|
|
|
6
|
-
constructor(endpoint: string, headers?: Record<string, string
|
|
9
|
+
constructor(endpoint: string, headers?: Record<string, string>, metrics?: MetricsCollector, verbose: boolean = false) {
|
|
7
10
|
this.client = new GraphQLClient(endpoint, {
|
|
8
11
|
headers: headers || {}
|
|
9
12
|
});
|
|
13
|
+
this.metrics = metrics;
|
|
14
|
+
this.verbose = verbose;
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
async executeMutation(mutation: string, variables: Record<string, any>): Promise<any> {
|
|
18
|
+
const startTime = Date.now();
|
|
19
|
+
|
|
13
20
|
try {
|
|
14
21
|
const result = await this.client.request(mutation, variables);
|
|
22
|
+
|
|
23
|
+
if (this.metrics) {
|
|
24
|
+
const duration = Date.now() - startTime;
|
|
25
|
+
this.metrics.recordRequestDuration(duration);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (this.verbose) {
|
|
29
|
+
console.log(`✓ GraphQL request completed in ${Date.now() - startTime}ms:`, result);
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
return result;
|
|
16
33
|
} catch (error) {
|
|
17
|
-
|
|
34
|
+
if (this.metrics) {
|
|
35
|
+
const duration = Date.now() - startTime;
|
|
36
|
+
this.metrics.recordRequestDuration(duration);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (this.verbose) {
|
|
40
|
+
console.error(`✗ GraphQL request failed in ${Date.now() - startTime}ms:`, error);
|
|
41
|
+
} else {
|
|
42
|
+
console.error('GraphQL mutation failed:', error);
|
|
43
|
+
}
|
|
18
44
|
throw error;
|
|
19
45
|
}
|
|
20
46
|
}
|