@jackchuka/gql-ingest 1.0.2 → 1.2.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/README.md +59 -5
- package/bin/cli.js +48 -1
- package/dist/config.d.ts +21 -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 +204 -0
- package/src/config.ts +90 -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,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
|
}
|
package/src/mapper.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { DataMapper } from "./mapper";
|
|
4
4
|
import { GraphQLClientWrapper } from "./graphql-client";
|
|
5
|
+
import { MetricsCollector } from "./metrics";
|
|
5
6
|
|
|
6
7
|
jest.mock("fs");
|
|
7
8
|
jest.mock("./csv-reader");
|
|
@@ -10,6 +11,7 @@ const mockFs = fs as jest.Mocked<typeof fs>;
|
|
|
10
11
|
|
|
11
12
|
describe("DataMapper", () => {
|
|
12
13
|
let mockClient: jest.Mocked<GraphQLClientWrapper>;
|
|
14
|
+
let mockMetrics: jest.Mocked<MetricsCollector>;
|
|
13
15
|
let dataMapper: DataMapper;
|
|
14
16
|
const testBasePath = "/test/base/path";
|
|
15
17
|
|
|
@@ -19,7 +21,15 @@ describe("DataMapper", () => {
|
|
|
19
21
|
setHeaders: jest.fn(),
|
|
20
22
|
} as any;
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
mockMetrics = {
|
|
25
|
+
startEntityProcessing: jest.fn(),
|
|
26
|
+
recordSuccess: jest.fn(),
|
|
27
|
+
recordFailure: jest.fn(),
|
|
28
|
+
finishEntityProcessing: jest.fn(),
|
|
29
|
+
getMetrics: jest.fn(),
|
|
30
|
+
} as any;
|
|
31
|
+
|
|
32
|
+
dataMapper = new DataMapper(mockClient, testBasePath, mockMetrics);
|
|
23
33
|
});
|
|
24
34
|
|
|
25
35
|
afterEach(() => {
|
|
@@ -165,7 +175,7 @@ describe("DataMapper", () => {
|
|
|
165
175
|
await dataMapper.processEntity("configs/test/mappings/users.json");
|
|
166
176
|
|
|
167
177
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
168
|
-
"✗ Failed to create entity for row:",
|
|
178
|
+
"✗ Failed to create entity for row 1:",
|
|
169
179
|
{ user_name: "John" },
|
|
170
180
|
expect.any(Error)
|
|
171
181
|
);
|
|
@@ -252,5 +262,74 @@ describe("DataMapper", () => {
|
|
|
252
262
|
email: "john@example.com",
|
|
253
263
|
});
|
|
254
264
|
});
|
|
265
|
+
|
|
266
|
+
it("should call metrics methods during successful processing", async () => {
|
|
267
|
+
const mockConfig = {
|
|
268
|
+
csvFile: "data/users.csv",
|
|
269
|
+
graphqlFile: "graphql/users.graphql",
|
|
270
|
+
mapping: { name: "user_name" },
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const mockCsvData = [{ user_name: "John" }, { user_name: "Jane" }];
|
|
274
|
+
const mockMutation =
|
|
275
|
+
"mutation CreateUser($name: String!) { createUser(input: { name: $name }) { id } }";
|
|
276
|
+
|
|
277
|
+
mockFs.readFileSync
|
|
278
|
+
.mockReturnValueOnce(JSON.stringify(mockConfig))
|
|
279
|
+
.mockReturnValueOnce(mockMutation);
|
|
280
|
+
|
|
281
|
+
const { readCsvFile } = require("./csv-reader");
|
|
282
|
+
readCsvFile.mockResolvedValue(mockCsvData);
|
|
283
|
+
|
|
284
|
+
mockClient.executeMutation.mockResolvedValue({
|
|
285
|
+
createUser: { id: "123" },
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await dataMapper.processEntity("configs/test/mappings/users.json");
|
|
289
|
+
|
|
290
|
+
expect(mockMetrics.startEntityProcessing).toHaveBeenCalledWith("users");
|
|
291
|
+
expect(mockMetrics.recordSuccess).toHaveBeenCalledTimes(2);
|
|
292
|
+
expect(mockMetrics.recordSuccess).toHaveBeenCalledWith("users");
|
|
293
|
+
expect(mockMetrics.finishEntityProcessing).toHaveBeenCalledWith("users");
|
|
294
|
+
expect(mockMetrics.recordFailure).not.toHaveBeenCalled();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("should call metrics methods during failed processing", async () => {
|
|
298
|
+
const mockConfig = {
|
|
299
|
+
csvFile: "data/users.csv",
|
|
300
|
+
graphqlFile: "graphql/users.graphql",
|
|
301
|
+
mapping: { name: "user_name" },
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const mockCsvData = [{ user_name: "John" }];
|
|
305
|
+
const mockMutation =
|
|
306
|
+
"mutation CreateUser($name: String!) { createUser(input: { name: $name }) { id } }";
|
|
307
|
+
|
|
308
|
+
mockFs.readFileSync
|
|
309
|
+
.mockReturnValueOnce(JSON.stringify(mockConfig))
|
|
310
|
+
.mockReturnValueOnce(mockMutation);
|
|
311
|
+
|
|
312
|
+
const { readCsvFile } = require("./csv-reader");
|
|
313
|
+
readCsvFile.mockResolvedValue(mockCsvData);
|
|
314
|
+
|
|
315
|
+
mockClient.executeMutation.mockRejectedValue(new Error("GraphQL error"));
|
|
316
|
+
|
|
317
|
+
const consoleSpy = jest.spyOn(console, "error").mockImplementation();
|
|
318
|
+
|
|
319
|
+
await dataMapper.processEntity("configs/test/mappings/users.json");
|
|
320
|
+
|
|
321
|
+
expect(mockMetrics.startEntityProcessing).toHaveBeenCalledWith("users");
|
|
322
|
+
expect(mockMetrics.recordFailure).toHaveBeenCalledTimes(1);
|
|
323
|
+
expect(mockMetrics.recordFailure).toHaveBeenCalledWith("users");
|
|
324
|
+
expect(mockMetrics.finishEntityProcessing).toHaveBeenCalledWith("users");
|
|
325
|
+
expect(mockMetrics.recordSuccess).not.toHaveBeenCalled();
|
|
326
|
+
|
|
327
|
+
consoleSpy.mockRestore();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("should expose metrics through getMetrics method", () => {
|
|
331
|
+
const metrics = dataMapper.getMetrics();
|
|
332
|
+
expect(metrics).toBe(mockMetrics);
|
|
333
|
+
});
|
|
255
334
|
});
|
|
256
335
|
});
|
package/src/mapper.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import { readCsvFile, CsvRow } from
|
|
4
|
-
import { GraphQLClientWrapper } from
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { readCsvFile, CsvRow } from "./csv-reader";
|
|
4
|
+
import { GraphQLClientWrapper } from "./graphql-client";
|
|
5
|
+
import { MetricsCollector } from "./metrics";
|
|
6
|
+
import { ParallelProcessingConfig } from "./config";
|
|
5
7
|
|
|
6
8
|
export interface MappingConfig {
|
|
7
9
|
csvFile: string;
|
|
@@ -12,69 +14,205 @@ export interface MappingConfig {
|
|
|
12
14
|
export class DataMapper {
|
|
13
15
|
private client: GraphQLClientWrapper;
|
|
14
16
|
private basePath: string;
|
|
17
|
+
private metrics: MetricsCollector;
|
|
18
|
+
private verbose: boolean;
|
|
15
19
|
|
|
16
|
-
constructor(
|
|
20
|
+
constructor(
|
|
21
|
+
client: GraphQLClientWrapper,
|
|
22
|
+
basePath: string = process.cwd(),
|
|
23
|
+
metrics?: MetricsCollector,
|
|
24
|
+
verbose: boolean = false
|
|
25
|
+
) {
|
|
17
26
|
this.client = client;
|
|
18
27
|
this.basePath = basePath;
|
|
28
|
+
this.metrics = metrics || new MetricsCollector();
|
|
29
|
+
this.verbose = verbose;
|
|
19
30
|
}
|
|
20
31
|
|
|
21
32
|
discoverMappings(configDir: string): string[] {
|
|
22
|
-
const mappingsPath = path.resolve(this.basePath, configDir,
|
|
23
|
-
|
|
33
|
+
const mappingsPath = path.resolve(this.basePath, configDir, "mappings");
|
|
34
|
+
|
|
24
35
|
try {
|
|
25
36
|
const files = fs.readdirSync(mappingsPath);
|
|
26
|
-
const jsonFiles = files
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return jsonFiles.map(file => path.join(configDir,
|
|
37
|
+
const jsonFiles = files.filter((file) => file.endsWith(".json")).sort(); // Alphabetical order for consistent processing
|
|
38
|
+
|
|
39
|
+
console.log(
|
|
40
|
+
`Discovered ${jsonFiles.length} mapping files: ${jsonFiles.join(", ")}`
|
|
41
|
+
);
|
|
42
|
+
return jsonFiles.map((file) => path.join(configDir, "mappings", file));
|
|
32
43
|
} catch (error) {
|
|
33
44
|
console.error(`Error reading mappings directory ${mappingsPath}:`, error);
|
|
34
45
|
return [];
|
|
35
46
|
}
|
|
36
47
|
}
|
|
37
48
|
|
|
38
|
-
async processEntity(
|
|
49
|
+
async processEntity(
|
|
50
|
+
configPath: string,
|
|
51
|
+
parallelConfig?: ParallelProcessingConfig
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
const entityName = path.basename(configPath, ".json");
|
|
39
54
|
console.log(`Processing entity: ${configPath}`);
|
|
40
|
-
|
|
55
|
+
|
|
56
|
+
this.metrics.startEntityProcessing(entityName);
|
|
57
|
+
|
|
41
58
|
// Read mapping configuration
|
|
42
59
|
const configFullPath = path.resolve(this.basePath, configPath);
|
|
43
|
-
const config: MappingConfig = JSON.parse(
|
|
44
|
-
|
|
60
|
+
const config: MappingConfig = JSON.parse(
|
|
61
|
+
fs.readFileSync(configFullPath, "utf8")
|
|
62
|
+
);
|
|
63
|
+
|
|
45
64
|
// Extract config directory (parent of mappings directory)
|
|
46
65
|
const configDir = path.dirname(path.dirname(configFullPath));
|
|
47
|
-
|
|
66
|
+
|
|
48
67
|
// Read CSV data (relative to config directory)
|
|
49
68
|
const csvPath = path.resolve(configDir, config.csvFile);
|
|
50
69
|
const csvData = await readCsvFile(csvPath);
|
|
51
|
-
|
|
70
|
+
|
|
52
71
|
// Read GraphQL mutation (relative to config directory)
|
|
53
72
|
const graphqlPath = path.resolve(configDir, config.graphqlFile);
|
|
54
|
-
const mutation = fs.readFileSync(graphqlPath,
|
|
73
|
+
const mutation = fs.readFileSync(graphqlPath, "utf8");
|
|
74
|
+
|
|
75
|
+
// Process rows with optional parallelization
|
|
76
|
+
if (parallelConfig && parallelConfig.concurrency > 1) {
|
|
77
|
+
await this.processRowsConcurrently(
|
|
78
|
+
csvData,
|
|
79
|
+
mutation,
|
|
80
|
+
config.mapping,
|
|
81
|
+
entityName,
|
|
82
|
+
parallelConfig
|
|
83
|
+
);
|
|
84
|
+
} else {
|
|
85
|
+
await this.processRowsSequentially(
|
|
86
|
+
csvData,
|
|
87
|
+
mutation,
|
|
88
|
+
config.mapping,
|
|
89
|
+
entityName
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.metrics.finishEntityProcessing(entityName);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async processRowsSequentially(
|
|
97
|
+
csvData: CsvRow[],
|
|
98
|
+
mutation: string,
|
|
99
|
+
mapping: Record<string, string>,
|
|
100
|
+
entityName: string
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
const totalRows = csvData.length;
|
|
55
103
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const variables = this.mapCsvRowToVariables(row,
|
|
59
|
-
|
|
104
|
+
for (let i = 0; i < csvData.length; i++) {
|
|
105
|
+
const row = csvData[i];
|
|
106
|
+
const variables = this.mapCsvRowToVariables(row, mapping);
|
|
107
|
+
|
|
60
108
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
109
|
+
await this.client.executeMutation(mutation, variables);
|
|
110
|
+
this.metrics.recordSuccess(entityName);
|
|
111
|
+
|
|
112
|
+
// Show progress every 10% or at the end (only in non-verbose mode)
|
|
113
|
+
if (!this.verbose && ((i + 1) % Math.max(1, Math.floor(totalRows / 10)) === 0 || i === totalRows - 1)) {
|
|
114
|
+
const progress = (((i + 1) / totalRows) * 100).toFixed(1);
|
|
115
|
+
console.log(`📊 Progress: ${i + 1}/${totalRows} (${progress}%) ✓`);
|
|
116
|
+
}
|
|
63
117
|
} catch (error) {
|
|
64
|
-
|
|
118
|
+
this.metrics.recordFailure(entityName);
|
|
119
|
+
if (!this.verbose) {
|
|
120
|
+
console.error(`✗ Failed to create entity for row ${i + 1}:`, row, error);
|
|
121
|
+
}
|
|
65
122
|
}
|
|
66
123
|
}
|
|
67
124
|
}
|
|
68
125
|
|
|
69
|
-
private
|
|
126
|
+
private async processRowsConcurrently(
|
|
127
|
+
csvData: CsvRow[],
|
|
128
|
+
mutation: string,
|
|
129
|
+
mapping: Record<string, string>,
|
|
130
|
+
entityName: string,
|
|
131
|
+
parallelConfig: ParallelProcessingConfig
|
|
132
|
+
): Promise<void> {
|
|
133
|
+
const concurrency = parallelConfig.concurrency;
|
|
134
|
+
console.log(
|
|
135
|
+
`Processing ${csvData.length} rows with concurrency: ${concurrency}`
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// Split data into chunks for concurrent processing
|
|
139
|
+
const chunks = this.chunkArray(csvData, concurrency);
|
|
140
|
+
let processedCount = 0;
|
|
141
|
+
const totalRows = csvData.length;
|
|
142
|
+
|
|
143
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
144
|
+
const chunk = chunks[chunkIndex];
|
|
145
|
+
const promises = chunk.map(async (row) => {
|
|
146
|
+
const variables = this.mapCsvRowToVariables(row, mapping);
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const result = await this.client.executeMutation(mutation, variables);
|
|
150
|
+
this.metrics.recordSuccess(entityName);
|
|
151
|
+
return { success: true, result, row };
|
|
152
|
+
} catch (error) {
|
|
153
|
+
this.metrics.recordFailure(entityName);
|
|
154
|
+
return { success: false, error, row };
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const results = await Promise.allSettled(promises);
|
|
159
|
+
processedCount += chunk.length;
|
|
160
|
+
|
|
161
|
+
// Count successes and failures in this chunk
|
|
162
|
+
let chunkSuccesses = 0;
|
|
163
|
+
let chunkFailures = 0;
|
|
164
|
+
|
|
165
|
+
results.forEach((result) => {
|
|
166
|
+
if (result.status === "fulfilled") {
|
|
167
|
+
const { success, error, row } = result.value;
|
|
168
|
+
if (success) {
|
|
169
|
+
chunkSuccesses++;
|
|
170
|
+
} else {
|
|
171
|
+
chunkFailures++;
|
|
172
|
+
if (!this.verbose) {
|
|
173
|
+
console.error(`✗ Failed to create entity for row:`, row, error);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
chunkFailures++;
|
|
178
|
+
if (!this.verbose) {
|
|
179
|
+
console.error(`✗ Promise rejected:`, result.reason);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Show progress update (only in non-verbose mode)
|
|
185
|
+
if (!this.verbose) {
|
|
186
|
+
const progress = ((processedCount / totalRows) * 100).toFixed(1);
|
|
187
|
+
console.log(`📊 Progress: ${processedCount}/${totalRows} (${progress}%) - Chunk ${chunkIndex + 1}: ${chunkSuccesses} ✓, ${chunkFailures} ✗`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private chunkArray<T>(array: T[], chunkSize: number): T[][] {
|
|
193
|
+
const chunks: T[][] = [];
|
|
194
|
+
for (let i = 0; i < array.length; i += chunkSize) {
|
|
195
|
+
chunks.push(array.slice(i, i + chunkSize));
|
|
196
|
+
}
|
|
197
|
+
return chunks;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private mapCsvRowToVariables(
|
|
201
|
+
row: CsvRow,
|
|
202
|
+
mapping: Record<string, string>
|
|
203
|
+
): Record<string, any> {
|
|
70
204
|
const variables: Record<string, any> = {};
|
|
71
|
-
|
|
205
|
+
|
|
72
206
|
for (const [graphqlVar, csvColumn] of Object.entries(mapping)) {
|
|
73
207
|
if (row[csvColumn] !== undefined) {
|
|
74
208
|
variables[graphqlVar] = row[csvColumn];
|
|
75
209
|
}
|
|
76
210
|
}
|
|
77
|
-
|
|
211
|
+
|
|
78
212
|
return variables;
|
|
79
213
|
}
|
|
80
|
-
|
|
214
|
+
|
|
215
|
+
getMetrics(): MetricsCollector {
|
|
216
|
+
return this.metrics;
|
|
217
|
+
}
|
|
218
|
+
}
|