@jackchuka/gql-ingest 2.1.0 → 2.2.1
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/README.md +7 -7
- package/dist/cli.js +237 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/dependency-resolver.d.ts.map +1 -1
- package/dist/graphql-client.d.ts.map +1 -1
- package/dist/mapper.d.ts +1 -1
- package/dist/mapper.d.ts.map +1 -1
- package/dist/metrics.d.ts.map +1 -1
- package/dist/readers/data-reader.d.ts.map +1 -1
- package/dist/readers/json.d.ts.map +1 -1
- package/dist/readers/jsonl.d.ts.map +1 -1
- package/dist/readers/yaml.d.ts.map +1 -1
- package/package.json +31 -25
- package/bin/cli.js +0 -231
- package/src/cli.ts +0 -187
- package/src/config.test.ts +0 -272
- package/src/config.ts +0 -125
- package/src/dependency-resolver.test.ts +0 -211
- package/src/dependency-resolver.ts +0 -102
- package/src/graphql-client.test.ts +0 -219
- package/src/graphql-client.ts +0 -151
- package/src/mapper.test.ts +0 -607
- package/src/mapper.ts +0 -489
- package/src/metrics.test.ts +0 -207
- package/src/metrics.ts +0 -161
- package/src/readers/csv.test.ts +0 -82
- package/src/readers/csv.ts +0 -29
- package/src/readers/data-reader.test.ts +0 -104
- package/src/readers/data-reader.ts +0 -61
- package/src/readers/index.ts +0 -18
- package/src/readers/json.test.ts +0 -80
- package/src/readers/json.ts +0 -27
- package/src/readers/jsonl.test.ts +0 -96
- package/src/readers/jsonl.ts +0 -28
- package/src/readers/yaml.test.ts +0 -95
- package/src/readers/yaml.ts +0 -28
package/src/cli.ts
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import { GraphQLClientWrapper } from "./graphql-client";
|
|
3
|
-
import { DataMapper } from "./mapper";
|
|
4
|
-
import { MetricsCollector } from "./metrics";
|
|
5
|
-
import { loadConfig, getEntityConfig, getRetryConfig } from "./config";
|
|
6
|
-
import { DependencyResolver } from "./dependency-resolver";
|
|
7
|
-
import { basename } from "path";
|
|
8
|
-
|
|
9
|
-
// Utility function to chunk array into smaller arrays
|
|
10
|
-
function chunkArray<T>(array: T[], chunkSize: number): T[][] {
|
|
11
|
-
if (chunkSize <= 0) return [array];
|
|
12
|
-
const chunks: T[][] = [];
|
|
13
|
-
for (let i = 0; i < array.length; i += chunkSize) {
|
|
14
|
-
chunks.push(array.slice(i, i + chunkSize));
|
|
15
|
-
}
|
|
16
|
-
return chunks;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const program = new Command();
|
|
20
|
-
|
|
21
|
-
program
|
|
22
|
-
.name("gql-ingest")
|
|
23
|
-
.description(
|
|
24
|
-
"A CLI tool for ingesting data from CSV files into a GraphQL API"
|
|
25
|
-
)
|
|
26
|
-
.version(require("../package.json").version);
|
|
27
|
-
|
|
28
|
-
program
|
|
29
|
-
.requiredOption("-e, --endpoint <url>", "GraphQL endpoint URL")
|
|
30
|
-
.requiredOption(
|
|
31
|
-
"-c, --config <path>",
|
|
32
|
-
"Path to configuration directory (containing data/, graphql/, mappings/ subdirectories)"
|
|
33
|
-
)
|
|
34
|
-
.option(
|
|
35
|
-
"-n, --entities <entities>",
|
|
36
|
-
"Comma-separated list of specific entities to process (e.g., users,products)"
|
|
37
|
-
)
|
|
38
|
-
.option(
|
|
39
|
-
"-h, --headers <headers>",
|
|
40
|
-
"JSON string of headers to include in requests"
|
|
41
|
-
)
|
|
42
|
-
.option("-v, --verbose", "Show detailed request results and responses")
|
|
43
|
-
.option(
|
|
44
|
-
"-f, --format <format>",
|
|
45
|
-
"Override data format detection (csv, json, yaml, jsonl)"
|
|
46
|
-
)
|
|
47
|
-
.action(async (options) => {
|
|
48
|
-
try {
|
|
49
|
-
console.log("Starting seed data generation...");
|
|
50
|
-
|
|
51
|
-
// Parse headers if provided
|
|
52
|
-
const headers = options.headers ? JSON.parse(options.headers) : {};
|
|
53
|
-
|
|
54
|
-
// Initialize metrics collector
|
|
55
|
-
const metrics = new MetricsCollector();
|
|
56
|
-
|
|
57
|
-
// Initialize GraphQL client
|
|
58
|
-
const client = new GraphQLClientWrapper(
|
|
59
|
-
options.endpoint,
|
|
60
|
-
headers,
|
|
61
|
-
metrics,
|
|
62
|
-
options.verbose
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
// Load configuration
|
|
66
|
-
const config = loadConfig(options.config);
|
|
67
|
-
|
|
68
|
-
// Initialize data mapper
|
|
69
|
-
const mapper = new DataMapper(
|
|
70
|
-
client,
|
|
71
|
-
process.cwd(),
|
|
72
|
-
metrics,
|
|
73
|
-
options.verbose,
|
|
74
|
-
options.format
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
// Parse entities filter if provided
|
|
78
|
-
const entityFilter = options.entities
|
|
79
|
-
? options.entities.split(",").map((e: string) => e.trim())
|
|
80
|
-
: undefined;
|
|
81
|
-
|
|
82
|
-
// Discover all mapping files dynamically
|
|
83
|
-
const mappingPaths = mapper.discoverMappings(
|
|
84
|
-
options.config,
|
|
85
|
-
entityFilter
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
if (mappingPaths.length === 0) {
|
|
89
|
-
const filterMsg = entityFilter
|
|
90
|
-
? ` matching entities: ${entityFilter.join(", ")}`
|
|
91
|
-
: "";
|
|
92
|
-
console.warn(
|
|
93
|
-
`No mapping files found in ${options.config}/mappings${filterMsg}`
|
|
94
|
-
);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Extract entity names from mapping paths
|
|
99
|
-
const entityNames = mappingPaths.map((path) => basename(path, ".json"));
|
|
100
|
-
|
|
101
|
-
// Filter dependencies to only include those relevant to selected entities
|
|
102
|
-
const relevantDependencies: Record<string, string[]> = {};
|
|
103
|
-
if (config.entityDependencies) {
|
|
104
|
-
for (const entity of entityNames) {
|
|
105
|
-
if (config.entityDependencies[entity]) {
|
|
106
|
-
relevantDependencies[entity] = config.entityDependencies[entity];
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Setup dependency resolver with filtered dependencies
|
|
112
|
-
const resolver = new DependencyResolver(
|
|
113
|
-
entityNames,
|
|
114
|
-
relevantDependencies,
|
|
115
|
-
!!entityFilter // Allow partial resolution when using --entities
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
// Validate dependencies
|
|
119
|
-
const validationErrors = resolver.validateDependencies();
|
|
120
|
-
if (validationErrors.length > 0) {
|
|
121
|
-
if (entityFilter) {
|
|
122
|
-
// When using --entities flag, show warnings instead of errors
|
|
123
|
-
console.warn("\n⚠️ Warning: Dependency validation issues:");
|
|
124
|
-
validationErrors.forEach((error) => console.warn(` - ${error}`));
|
|
125
|
-
console.warn(
|
|
126
|
-
"This may cause errors if the dependent data doesn't already exist.\n"
|
|
127
|
-
);
|
|
128
|
-
} else {
|
|
129
|
-
// Strict validation when processing all entities
|
|
130
|
-
console.error("Dependency validation errors:");
|
|
131
|
-
validationErrors.forEach((error) => console.error(` - ${error}`));
|
|
132
|
-
process.exit(1);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
await processEntitiesInWaves(mappingPaths, resolver, mapper, config);
|
|
137
|
-
|
|
138
|
-
metrics.finishProcessing();
|
|
139
|
-
console.log(metrics.generateSummary());
|
|
140
|
-
} catch (error) {
|
|
141
|
-
console.error("Error:", error);
|
|
142
|
-
process.exit(1);
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
async function processEntitiesInWaves(
|
|
147
|
-
mappingPaths: string[],
|
|
148
|
-
resolver: DependencyResolver,
|
|
149
|
-
mapper: DataMapper,
|
|
150
|
-
config: ReturnType<typeof loadConfig>
|
|
151
|
-
): Promise<void> {
|
|
152
|
-
const waves = resolver.resolveExecutionOrder();
|
|
153
|
-
const pathMap = new Map(
|
|
154
|
-
mappingPaths.map((path) => [basename(path, ".json"), path])
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
console.log(`Processing ${waves.length} dependency waves...`);
|
|
158
|
-
|
|
159
|
-
for (const wave of waves) {
|
|
160
|
-
console.log(
|
|
161
|
-
`Wave ${wave.wave + 1}: Processing entities [${wave.entities.join(", ")}]`
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
// Process entities in controlled batches based on entityConcurrency
|
|
165
|
-
const entityConcurrency = config.parallelProcessing.entityConcurrency;
|
|
166
|
-
const chunks = chunkArray(wave.entities, entityConcurrency);
|
|
167
|
-
|
|
168
|
-
for (const chunk of chunks) {
|
|
169
|
-
const entityPromises = chunk.map(async (entityName) => {
|
|
170
|
-
const configPath = pathMap.get(entityName);
|
|
171
|
-
if (configPath) {
|
|
172
|
-
try {
|
|
173
|
-
const entityConfig = getEntityConfig(entityName, config);
|
|
174
|
-
const retryConfig = getRetryConfig(entityName, config);
|
|
175
|
-
await mapper.processEntity(configPath, entityConfig, retryConfig);
|
|
176
|
-
} catch (error) {
|
|
177
|
-
console.warn(`Warning: Could not process ${configPath}:`, error);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
await Promise.allSettled(entityPromises);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
program.parse();
|
package/src/config.test.ts
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { loadConfig, getEntityConfig, getRetryConfig, 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
|
-
entityConcurrency: 3
|
|
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.entityConcurrency).toBe(3);
|
|
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.entityConcurrency).toBe(1); // 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
|
-
retry: {
|
|
116
|
-
maxAttempts: 3,
|
|
117
|
-
baseDelay: 1000,
|
|
118
|
-
maxDelay: 30000,
|
|
119
|
-
exponentialBackoff: true,
|
|
120
|
-
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
121
|
-
},
|
|
122
|
-
parallelProcessing: {
|
|
123
|
-
concurrency: 10,
|
|
124
|
-
entityConcurrency: 3,
|
|
125
|
-
preserveRowOrder: false,
|
|
126
|
-
},
|
|
127
|
-
entityConfig: {
|
|
128
|
-
users: {
|
|
129
|
-
concurrency: 2,
|
|
130
|
-
preserveRowOrder: true,
|
|
131
|
-
},
|
|
132
|
-
products: {
|
|
133
|
-
concurrency: 20,
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
entityDependencies: {},
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
it("should return global config for entity without overrides", () => {
|
|
140
|
-
const entityConfig = getEntityConfig("orders", globalConfig);
|
|
141
|
-
|
|
142
|
-
expect(entityConfig).toEqual(globalConfig.parallelProcessing);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("should merge entity overrides with global config", () => {
|
|
146
|
-
const entityConfig = getEntityConfig("products", globalConfig);
|
|
147
|
-
|
|
148
|
-
expect(entityConfig.concurrency).toBe(20); // overridden
|
|
149
|
-
expect(entityConfig.entityConcurrency).toBe(3); // from global
|
|
150
|
-
expect(entityConfig.preserveRowOrder).toBe(false); // from global
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("should apply preserveRowOrder constraint", () => {
|
|
154
|
-
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
155
|
-
|
|
156
|
-
const entityConfig = getEntityConfig("users", globalConfig);
|
|
157
|
-
|
|
158
|
-
expect(entityConfig.concurrency).toBe(1); // forced to 1
|
|
159
|
-
expect(entityConfig.preserveRowOrder).toBe(true);
|
|
160
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
161
|
-
"Entity 'users': preserveRowOrder=true forces concurrency=1 (was 2)"
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
consoleSpy.mockRestore();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("should not apply constraint when concurrency is already 1", () => {
|
|
168
|
-
const config = {
|
|
169
|
-
...globalConfig,
|
|
170
|
-
entityConfig: {
|
|
171
|
-
...globalConfig.entityConfig,
|
|
172
|
-
sequential: {
|
|
173
|
-
concurrency: 1,
|
|
174
|
-
preserveRowOrder: true,
|
|
175
|
-
},
|
|
176
|
-
},
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
180
|
-
|
|
181
|
-
const entityConfig = getEntityConfig("sequential", config);
|
|
182
|
-
|
|
183
|
-
expect(entityConfig.concurrency).toBe(1);
|
|
184
|
-
expect(consoleSpy).not.toHaveBeenCalled();
|
|
185
|
-
|
|
186
|
-
consoleSpy.mockRestore();
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("should not apply constraint when preserveRowOrder is false", () => {
|
|
190
|
-
const config = {
|
|
191
|
-
...globalConfig,
|
|
192
|
-
entityConfig: {
|
|
193
|
-
...globalConfig.entityConfig,
|
|
194
|
-
bulk: {
|
|
195
|
-
concurrency: 50,
|
|
196
|
-
preserveRowOrder: false,
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
202
|
-
|
|
203
|
-
const entityConfig = getEntityConfig("bulk", config);
|
|
204
|
-
|
|
205
|
-
expect(entityConfig.concurrency).toBe(50);
|
|
206
|
-
expect(consoleSpy).not.toHaveBeenCalled();
|
|
207
|
-
|
|
208
|
-
consoleSpy.mockRestore();
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
describe("getRetryConfig", () => {
|
|
213
|
-
const globalConfig = {
|
|
214
|
-
retry: {
|
|
215
|
-
maxAttempts: 3,
|
|
216
|
-
baseDelay: 1000,
|
|
217
|
-
maxDelay: 30000,
|
|
218
|
-
exponentialBackoff: true,
|
|
219
|
-
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
220
|
-
},
|
|
221
|
-
parallelProcessing: {
|
|
222
|
-
concurrency: 10,
|
|
223
|
-
entityConcurrency: 3,
|
|
224
|
-
preserveRowOrder: false,
|
|
225
|
-
},
|
|
226
|
-
entityConfig: {
|
|
227
|
-
important: {
|
|
228
|
-
retry: {
|
|
229
|
-
maxAttempts: 5,
|
|
230
|
-
baseDelay: 500,
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
fast: {
|
|
234
|
-
retry: {
|
|
235
|
-
maxAttempts: 1,
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
entityDependencies: {},
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
it("should return global retry config for entity without overrides", () => {
|
|
243
|
-
const retryConfig = getRetryConfig("regular", globalConfig);
|
|
244
|
-
|
|
245
|
-
expect(retryConfig).toEqual(globalConfig.retry);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("should merge entity retry overrides with global config", () => {
|
|
249
|
-
const retryConfig = getRetryConfig("important", globalConfig);
|
|
250
|
-
|
|
251
|
-
expect(retryConfig.maxAttempts).toBe(5); // overridden
|
|
252
|
-
expect(retryConfig.baseDelay).toBe(500); // overridden
|
|
253
|
-
expect(retryConfig.maxDelay).toBe(30000); // from global
|
|
254
|
-
expect(retryConfig.exponentialBackoff).toBe(true); // from global
|
|
255
|
-
expect(retryConfig.retryableStatusCodes).toEqual([408, 429, 500, 502, 503, 504]); // from global
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it("should handle partial retry overrides", () => {
|
|
259
|
-
const retryConfig = getRetryConfig("fast", globalConfig);
|
|
260
|
-
|
|
261
|
-
expect(retryConfig.maxAttempts).toBe(1); // overridden
|
|
262
|
-
expect(retryConfig.baseDelay).toBe(1000); // from global
|
|
263
|
-
expect(retryConfig.maxDelay).toBe(30000); // from global
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it("should handle entity with no retry config", () => {
|
|
267
|
-
const retryConfig = getRetryConfig("undefined-entity", globalConfig);
|
|
268
|
-
|
|
269
|
-
expect(retryConfig).toEqual(globalConfig.retry);
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
});
|
package/src/config.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
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
|
-
entityConcurrency: number;
|
|
8
|
-
preserveRowOrder: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface RetryConfig {
|
|
12
|
-
maxAttempts: number;
|
|
13
|
-
baseDelay: number;
|
|
14
|
-
maxDelay: number;
|
|
15
|
-
exponentialBackoff: boolean;
|
|
16
|
-
retryableStatusCodes: number[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface EntityConfig {
|
|
20
|
-
concurrency?: number;
|
|
21
|
-
preserveRowOrder?: boolean;
|
|
22
|
-
retry?: Partial<RetryConfig>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface ProcessingConfig {
|
|
26
|
-
retry: RetryConfig;
|
|
27
|
-
parallelProcessing: ParallelProcessingConfig;
|
|
28
|
-
entityConfig: Record<string, EntityConfig>;
|
|
29
|
-
entityDependencies: Record<string, string[]>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface FullConfig extends ProcessingConfig {
|
|
33
|
-
// Future: additional config sections can be added here
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export const DEFAULT_RETRY_CONFIG: RetryConfig = {
|
|
37
|
-
maxAttempts: 3,
|
|
38
|
-
baseDelay: 1000,
|
|
39
|
-
maxDelay: 30000,
|
|
40
|
-
exponentialBackoff: true,
|
|
41
|
-
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export const DEFAULT_PARALLEL_CONFIG: ParallelProcessingConfig = {
|
|
45
|
-
concurrency: 1,
|
|
46
|
-
entityConcurrency: 1,
|
|
47
|
-
preserveRowOrder: true,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export const DEFAULT_CONFIG: ProcessingConfig = {
|
|
51
|
-
retry: DEFAULT_RETRY_CONFIG,
|
|
52
|
-
parallelProcessing: DEFAULT_PARALLEL_CONFIG,
|
|
53
|
-
entityConfig: {},
|
|
54
|
-
entityDependencies: {},
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export function loadConfig(configDir: string): ProcessingConfig {
|
|
58
|
-
const configPath = path.join(configDir, "config.yaml");
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
if (!fs.existsSync(configPath)) {
|
|
62
|
-
console.log("No config.yaml found, using default sequential processing");
|
|
63
|
-
return DEFAULT_CONFIG;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const fileContents = fs.readFileSync(configPath, "utf8");
|
|
67
|
-
const yamlConfig = yaml.load(fileContents) as Partial<FullConfig>;
|
|
68
|
-
|
|
69
|
-
return mergeWithDefaults(yamlConfig);
|
|
70
|
-
} catch (error) {
|
|
71
|
-
console.warn(
|
|
72
|
-
`Warning: Failed to parse config.yaml: ${error}. Using defaults.`
|
|
73
|
-
);
|
|
74
|
-
return DEFAULT_CONFIG;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function mergeWithDefaults(yamlConfig: Partial<FullConfig>): ProcessingConfig {
|
|
79
|
-
return {
|
|
80
|
-
retry: {
|
|
81
|
-
...DEFAULT_RETRY_CONFIG,
|
|
82
|
-
...(yamlConfig.retry || {}),
|
|
83
|
-
},
|
|
84
|
-
parallelProcessing: {
|
|
85
|
-
...DEFAULT_PARALLEL_CONFIG,
|
|
86
|
-
...(yamlConfig.parallelProcessing || {}),
|
|
87
|
-
},
|
|
88
|
-
entityConfig: yamlConfig.entityConfig || {},
|
|
89
|
-
entityDependencies: yamlConfig.entityDependencies || {},
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function getEntityConfig(
|
|
94
|
-
entityName: string,
|
|
95
|
-
globalConfig: ProcessingConfig
|
|
96
|
-
): ParallelProcessingConfig {
|
|
97
|
-
const entityOverrides = globalConfig.entityConfig[entityName] || {};
|
|
98
|
-
|
|
99
|
-
let finalConfig = {
|
|
100
|
-
...globalConfig.parallelProcessing,
|
|
101
|
-
...entityOverrides,
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// Apply constraint: preserveRowOrder forces concurrency = 1
|
|
105
|
-
if (finalConfig.preserveRowOrder && finalConfig.concurrency > 1) {
|
|
106
|
-
console.warn(
|
|
107
|
-
`Entity '${entityName}': preserveRowOrder=true forces concurrency=1 (was ${finalConfig.concurrency})`
|
|
108
|
-
);
|
|
109
|
-
finalConfig.concurrency = 1;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return finalConfig;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function getRetryConfig(
|
|
116
|
-
entityName: string,
|
|
117
|
-
globalConfig: ProcessingConfig
|
|
118
|
-
): RetryConfig {
|
|
119
|
-
const entityOverrides = globalConfig.entityConfig[entityName]?.retry || {};
|
|
120
|
-
|
|
121
|
-
return {
|
|
122
|
-
...globalConfig.retry,
|
|
123
|
-
...entityOverrides,
|
|
124
|
-
};
|
|
125
|
-
}
|