@jackchuka/gql-ingest 2.1.0 → 2.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.
@@ -1,607 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { DataMapper } from "./mapper";
4
- import { GraphQLClientWrapper } from "./graphql-client";
5
- import { MetricsCollector } from "./metrics";
6
- import { readCsvFile, DataReaderFactory } from "./readers";
7
-
8
- jest.mock("fs");
9
- jest.mock("./readers", () => ({
10
- ...jest.requireActual("./readers"),
11
- readCsvFile: jest.fn(),
12
- DataReaderFactory: {
13
- getReader: jest.fn().mockReturnValue({
14
- readFile: jest.fn(),
15
- }),
16
- },
17
- }));
18
-
19
- const mockFs = fs as jest.Mocked<typeof fs>;
20
-
21
- describe("DataMapper", () => {
22
- let mockClient: jest.Mocked<GraphQLClientWrapper>;
23
- let mockMetrics: jest.Mocked<MetricsCollector>;
24
- let dataMapper: DataMapper;
25
- const testBasePath = "/test/base/path";
26
-
27
- beforeEach(() => {
28
- mockClient = {
29
- executeMutation: jest.fn(),
30
- setHeaders: jest.fn(),
31
- } as any;
32
-
33
- mockMetrics = {
34
- startEntityProcessing: jest.fn(),
35
- recordSuccess: jest.fn(),
36
- recordFailure: jest.fn(),
37
- finishEntityProcessing: jest.fn(),
38
- getMetrics: jest.fn(),
39
- } as any;
40
-
41
- dataMapper = new DataMapper(mockClient, testBasePath, mockMetrics);
42
- });
43
-
44
- afterEach(() => {
45
- jest.clearAllMocks();
46
- });
47
-
48
- describe("discoverMappings", () => {
49
- it("should discover mapping files in alphabetical order", () => {
50
- const mockFiles = ["users.json", "items.json", "orders.json"];
51
- mockFs.readdirSync.mockReturnValue(mockFiles as any);
52
-
53
- const consoleSpy = jest.spyOn(console, "log").mockImplementation();
54
-
55
- const result = dataMapper.discoverMappings("configs/test");
56
-
57
- expect(mockFs.readdirSync).toHaveBeenCalledWith(
58
- path.resolve(testBasePath, "configs/test", "mappings")
59
- );
60
- expect(result).toEqual([
61
- "configs/test/mappings/items.json",
62
- "configs/test/mappings/orders.json",
63
- "configs/test/mappings/users.json",
64
- ]);
65
- expect(consoleSpy).toHaveBeenCalledWith(
66
- "Discovered 3 mapping files: items.json, orders.json, users.json"
67
- );
68
-
69
- consoleSpy.mockRestore();
70
- });
71
-
72
- it("should filter only JSON files", () => {
73
- const mockFiles = ["users.json", "items.txt", "orders.json", "readme.md"];
74
- mockFs.readdirSync.mockReturnValue(mockFiles as any);
75
-
76
- const result = dataMapper.discoverMappings("configs/test");
77
-
78
- expect(result).toEqual([
79
- "configs/test/mappings/orders.json",
80
- "configs/test/mappings/users.json",
81
- ]);
82
- });
83
-
84
- it("should handle directory read errors", () => {
85
- mockFs.readdirSync.mockImplementation(() => {
86
- throw new Error("Directory not found");
87
- });
88
-
89
- const consoleSpy = jest.spyOn(console, "error").mockImplementation();
90
-
91
- const result = dataMapper.discoverMappings("configs/nonexistent");
92
-
93
- expect(result).toEqual([]);
94
- expect(consoleSpy).toHaveBeenCalledWith(
95
- expect.stringContaining("Error reading mappings directory"),
96
- expect.any(Error)
97
- );
98
-
99
- consoleSpy.mockRestore();
100
- });
101
- });
102
-
103
- describe("processEntity", () => {
104
- it("should process entity successfully", async () => {
105
- const mockConfig = {
106
- csvFile: "data/users.csv",
107
- graphqlFile: "graphql/users.graphql",
108
- mapping: {
109
- name: "user_name",
110
- email: "user_email",
111
- },
112
- };
113
-
114
- const mockCsvData = [
115
- { user_name: "John", user_email: "john@example.com" },
116
- { user_name: "Jane", user_email: "jane@example.com" },
117
- ];
118
-
119
- const mockMutation =
120
- "mutation CreateUser($name: String!, $email: String!) { createUser(input: { name: $name, email: $email }) { id } }";
121
-
122
- mockFs.readFileSync
123
- .mockReturnValueOnce(JSON.stringify(mockConfig))
124
- .mockReturnValueOnce(mockMutation);
125
-
126
- const { DataReaderFactory } = require("./readers");
127
- DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
128
-
129
- mockClient.executeMutation.mockResolvedValue({
130
- createUser: { id: "123" },
131
- });
132
-
133
- const consoleSpy = jest.spyOn(console, "log").mockImplementation();
134
-
135
- await dataMapper.processEntity("configs/test/mappings/users.json");
136
-
137
- expect(mockFs.readFileSync).toHaveBeenCalledWith(
138
- path.resolve(testBasePath, "configs/test/mappings/users.json"),
139
- "utf8"
140
- );
141
- expect(DataReaderFactory.getReader).toHaveBeenCalledWith(
142
- path.resolve(testBasePath, "configs/test", "data/users.csv"),
143
- undefined
144
- );
145
- expect(mockFs.readFileSync).toHaveBeenCalledWith(
146
- path.resolve(testBasePath, "configs/test", "graphql/users.graphql"),
147
- "utf8"
148
- );
149
-
150
- expect(mockClient.executeMutation).toHaveBeenCalledTimes(2);
151
- expect(mockClient.executeMutation).toHaveBeenCalledWith(
152
- mockMutation,
153
- {
154
- name: "John",
155
- email: "john@example.com",
156
- },
157
- undefined
158
- );
159
- expect(mockClient.executeMutation).toHaveBeenCalledWith(
160
- mockMutation,
161
- {
162
- name: "Jane",
163
- email: "jane@example.com",
164
- },
165
- undefined
166
- );
167
-
168
- consoleSpy.mockRestore();
169
- });
170
-
171
- it("should handle GraphQL execution errors gracefully", async () => {
172
- const mockConfig = {
173
- csvFile: "data/users.csv",
174
- graphqlFile: "graphql/users.graphql",
175
- mapping: { name: "user_name" },
176
- };
177
-
178
- const mockCsvData = [{ user_name: "John" }];
179
- const mockMutation =
180
- "mutation CreateUser($name: String!) { createUser(input: { name: $name }) { id } }";
181
-
182
- mockFs.readFileSync
183
- .mockReturnValueOnce(JSON.stringify(mockConfig))
184
- .mockReturnValueOnce(mockMutation);
185
-
186
- const { DataReaderFactory } = require("./readers");
187
- DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
188
-
189
- mockClient.executeMutation.mockRejectedValue(new Error("GraphQL error"));
190
-
191
- const consoleSpy = jest.spyOn(console, "error").mockImplementation();
192
-
193
- await dataMapper.processEntity("configs/test/mappings/users.json");
194
-
195
- expect(consoleSpy).toHaveBeenCalledWith(
196
- "✗ Failed to create entity for row 1:",
197
- { user_name: "John" },
198
- expect.any(Error)
199
- );
200
-
201
- consoleSpy.mockRestore();
202
- });
203
-
204
- it("should map CSV columns to GraphQL variables correctly", async () => {
205
- const mockConfig = {
206
- csvFile: "data/products.csv",
207
- graphqlFile: "graphql/products.graphql",
208
- mapping: {
209
- name: "product_name",
210
- price: "product_price",
211
- sku: "product_sku",
212
- },
213
- };
214
-
215
- const mockCsvData = [
216
- {
217
- product_name: "Widget",
218
- product_price: "19.99",
219
- product_sku: "W001",
220
- extra_column: "ignored",
221
- },
222
- ];
223
-
224
- const mockMutation =
225
- "mutation CreateProduct($name: String!, $price: String!, $sku: String!) { createProduct(input: { name: $name, price: $price, sku: $sku }) { id } }";
226
-
227
- mockFs.readFileSync
228
- .mockReturnValueOnce(JSON.stringify(mockConfig))
229
- .mockReturnValueOnce(mockMutation);
230
-
231
- const { DataReaderFactory } = require("./readers");
232
- DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
233
-
234
- mockClient.executeMutation.mockResolvedValue({
235
- createProduct: { id: "456" },
236
- });
237
-
238
- await dataMapper.processEntity("configs/test/mappings/products.json");
239
-
240
- expect(mockClient.executeMutation).toHaveBeenCalledWith(
241
- mockMutation,
242
- {
243
- name: "Widget",
244
- price: "19.99",
245
- sku: "W001",
246
- },
247
- undefined
248
- );
249
- });
250
-
251
- it("should handle missing CSV columns gracefully", async () => {
252
- const mockConfig = {
253
- csvFile: "data/users.csv",
254
- graphqlFile: "graphql/users.graphql",
255
- mapping: {
256
- name: "user_name",
257
- email: "user_email",
258
- phone: "user_phone",
259
- },
260
- };
261
-
262
- const mockCsvData = [
263
- { user_name: "John", user_email: "john@example.com" },
264
- ];
265
-
266
- const mockMutation =
267
- "mutation CreateUser($name: String!, $email: String, $phone: String) { createUser(input: { name: $name, email: $email, phone: $phone }) { id } }";
268
-
269
- mockFs.readFileSync
270
- .mockReturnValueOnce(JSON.stringify(mockConfig))
271
- .mockReturnValueOnce(mockMutation);
272
-
273
- const { DataReaderFactory } = require("./readers");
274
- DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
275
-
276
- mockClient.executeMutation.mockResolvedValue({
277
- createUser: { id: "789" },
278
- });
279
-
280
- await dataMapper.processEntity("configs/test/mappings/users.json");
281
-
282
- expect(mockClient.executeMutation).toHaveBeenCalledWith(
283
- mockMutation,
284
- {
285
- name: "John",
286
- email: "john@example.com",
287
- },
288
- undefined
289
- );
290
- });
291
-
292
- it("should call metrics methods during successful processing", async () => {
293
- const mockConfig = {
294
- csvFile: "data/users.csv",
295
- graphqlFile: "graphql/users.graphql",
296
- mapping: { name: "user_name" },
297
- };
298
-
299
- const mockCsvData = [{ user_name: "John" }, { user_name: "Jane" }];
300
- const mockMutation =
301
- "mutation CreateUser($name: String!) { createUser(input: { name: $name }) { id } }";
302
-
303
- mockFs.readFileSync
304
- .mockReturnValueOnce(JSON.stringify(mockConfig))
305
- .mockReturnValueOnce(mockMutation);
306
-
307
- const { DataReaderFactory } = require("./readers");
308
- DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
309
-
310
- mockClient.executeMutation.mockResolvedValue({
311
- createUser: { id: "123" },
312
- });
313
-
314
- await dataMapper.processEntity("configs/test/mappings/users.json");
315
-
316
- expect(mockMetrics.startEntityProcessing).toHaveBeenCalledWith("users");
317
- expect(mockMetrics.recordSuccess).toHaveBeenCalledTimes(2);
318
- expect(mockMetrics.recordSuccess).toHaveBeenCalledWith("users");
319
- expect(mockMetrics.finishEntityProcessing).toHaveBeenCalledWith("users");
320
- expect(mockMetrics.recordFailure).not.toHaveBeenCalled();
321
- });
322
-
323
- it("should call metrics methods during failed processing", async () => {
324
- const mockConfig = {
325
- csvFile: "data/users.csv",
326
- graphqlFile: "graphql/users.graphql",
327
- mapping: { name: "user_name" },
328
- };
329
-
330
- const mockCsvData = [{ user_name: "John" }];
331
- const mockMutation =
332
- "mutation CreateUser($name: String!) { createUser(input: { name: $name }) { id } }";
333
-
334
- mockFs.readFileSync
335
- .mockReturnValueOnce(JSON.stringify(mockConfig))
336
- .mockReturnValueOnce(mockMutation);
337
-
338
- const { DataReaderFactory } = require("./readers");
339
- DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
340
-
341
- mockClient.executeMutation.mockRejectedValue(new Error("GraphQL error"));
342
-
343
- const consoleSpy = jest.spyOn(console, "error").mockImplementation();
344
-
345
- await dataMapper.processEntity("configs/test/mappings/users.json");
346
-
347
- expect(mockMetrics.startEntityProcessing).toHaveBeenCalledWith("users");
348
- expect(mockMetrics.recordFailure).toHaveBeenCalledTimes(1);
349
- expect(mockMetrics.recordFailure).toHaveBeenCalledWith("users");
350
- expect(mockMetrics.finishEntityProcessing).toHaveBeenCalledWith("users");
351
- expect(mockMetrics.recordSuccess).not.toHaveBeenCalled();
352
-
353
- consoleSpy.mockRestore();
354
- });
355
-
356
- it("should expose metrics through getMetrics method", () => {
357
- const metrics = dataMapper.getMetrics();
358
- expect(metrics).toBe(mockMetrics);
359
- });
360
-
361
- it("should convert numeric types from CSV strings to proper GraphQL types", async () => {
362
- const mockConfig = {
363
- csvFile: "data/products.csv",
364
- graphqlFile: "graphql/products.graphql",
365
- mapping: {
366
- name: "product_name",
367
- price: "product_price",
368
- quantity: "product_quantity",
369
- active: "product_active",
370
- },
371
- };
372
-
373
- const mockCsvData = [
374
- {
375
- product_name: "Widget",
376
- product_price: "19.99",
377
- product_quantity: "10",
378
- product_active: "true",
379
- },
380
- ];
381
-
382
- const mockMutation = `
383
- mutation CreateProduct($name: String!, $price: Float!, $quantity: Int!, $active: Boolean!) {
384
- createProduct(input: { name: $name, price: $price, quantity: $quantity, active: $active }) {
385
- id
386
- }
387
- }
388
- `;
389
-
390
- mockFs.readFileSync
391
- .mockReturnValueOnce(JSON.stringify(mockConfig))
392
- .mockReturnValueOnce(mockMutation);
393
-
394
- const { DataReaderFactory } = require("./readers");
395
- DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
396
-
397
- mockClient.executeMutation.mockResolvedValue({
398
- createProduct: { id: "123" },
399
- });
400
-
401
- await dataMapper.processEntity("configs/test/mappings/products.json");
402
-
403
- expect(mockClient.executeMutation).toHaveBeenCalledWith(
404
- mockMutation,
405
- {
406
- name: "Widget",
407
- price: 19.99,
408
- quantity: 10,
409
- active: true,
410
- },
411
- undefined
412
- );
413
- });
414
-
415
- it("should handle invalid numeric conversions gracefully", async () => {
416
- const mockConfig = {
417
- csvFile: "data/products.csv",
418
- graphqlFile: "graphql/products.graphql",
419
- mapping: {
420
- name: "product_name",
421
- price: "product_price",
422
- quantity: "product_quantity",
423
- },
424
- };
425
-
426
- const mockCsvData = [
427
- {
428
- product_name: "Widget",
429
- product_price: "invalid_price",
430
- product_quantity: "invalid_quantity",
431
- },
432
- ];
433
-
434
- const mockMutation = `
435
- mutation CreateProduct($name: String!, $price: Float!, $quantity: Int!) {
436
- createProduct(input: { name: $name, price: $price, quantity: $quantity }) {
437
- id
438
- }
439
- }
440
- `;
441
-
442
- mockFs.readFileSync
443
- .mockReturnValueOnce(JSON.stringify(mockConfig))
444
- .mockReturnValueOnce(mockMutation);
445
-
446
- const { DataReaderFactory } = require("./readers");
447
- DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
448
-
449
- mockClient.executeMutation.mockResolvedValue({
450
- createProduct: { id: "123" },
451
- });
452
-
453
- const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
454
-
455
- await dataMapper.processEntity("configs/test/mappings/products.json");
456
-
457
- expect(mockClient.executeMutation).toHaveBeenCalledWith(
458
- mockMutation,
459
- {
460
- name: "Widget",
461
- price: "invalid_price",
462
- quantity: "invalid_quantity",
463
- },
464
- undefined
465
- );
466
-
467
- expect(consoleSpy).toHaveBeenCalledWith(
468
- 'Warning: Cannot convert "invalid_price" to Float for variable $price. Expected a valid number. Using original value.'
469
- );
470
- expect(consoleSpy).toHaveBeenCalledWith(
471
- 'Warning: Cannot convert "invalid_quantity" to Int for variable $quantity. Expected a valid integer. Using original value.'
472
- );
473
-
474
- consoleSpy.mockRestore();
475
- });
476
-
477
- it("should handle edge cases in numeric conversion safely", async () => {
478
- const mockConfig = {
479
- csvFile: "data/products.csv",
480
- graphqlFile: "graphql/products.graphql",
481
- mapping: {
482
- int_field: "int_value",
483
- float_field: "float_value",
484
- },
485
- };
486
-
487
- const mockCsvData = [
488
- {
489
- int_value: "1.5", // Float in Int field - should remain string
490
- float_value: "Infinity", // Invalid float - should remain string
491
- },
492
- {
493
- int_value: "not_a_number", // Invalid int - should remain string
494
- float_value: "1.2.3", // Invalid number format - should remain string
495
- },
496
- ];
497
-
498
- const mockMutation = `
499
- mutation CreateProduct($int_field: Int!, $float_field: Float!) {
500
- createProduct(input: { int_field: $int_field, float_field: $float_field }) {
501
- id
502
- }
503
- }
504
- `;
505
-
506
- mockFs.readFileSync
507
- .mockReturnValueOnce(JSON.stringify(mockConfig))
508
- .mockReturnValueOnce(mockMutation);
509
-
510
- const { DataReaderFactory } = require("./readers");
511
- DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
512
-
513
- mockClient.executeMutation.mockResolvedValue({
514
- createProduct: { id: "123" },
515
- });
516
-
517
- const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
518
-
519
- await dataMapper.processEntity("configs/test/mappings/products.json");
520
-
521
- // Should keep invalid values as strings
522
- expect(mockClient.executeMutation).toHaveBeenCalledWith(
523
- mockMutation,
524
- {
525
- int_field: "1.5",
526
- float_field: "Infinity",
527
- },
528
- undefined
529
- );
530
-
531
- expect(mockClient.executeMutation).toHaveBeenCalledWith(
532
- mockMutation,
533
- {
534
- int_field: "not_a_number",
535
- float_field: "1.2.3",
536
- },
537
- undefined
538
- );
539
-
540
- consoleSpy.mockRestore();
541
- });
542
-
543
- it("should keep unknown scalar types as strings", async () => {
544
- const mockConfig = {
545
- csvFile: "data/products.csv",
546
- graphqlFile: "graphql/products.graphql",
547
- mapping: {
548
- name: "product_name",
549
- custom_field: "custom_value",
550
- },
551
- };
552
-
553
- const mockCsvData = [
554
- {
555
- product_name: "Widget",
556
- custom_value: "123",
557
- },
558
- ];
559
-
560
- const mockMutation = `
561
- mutation CreateProduct($name: String!, $custom_field: CustomScalar!) {
562
- createProduct(input: { name: $name, custom_field: $custom_field }) {
563
- id
564
- }
565
- }
566
- `;
567
-
568
- mockFs.readFileSync
569
- .mockReturnValueOnce(JSON.stringify(mockConfig))
570
- .mockReturnValueOnce(mockMutation);
571
-
572
- const { DataReaderFactory } = require("./readers");
573
- DataReaderFactory.getReader().readFile.mockResolvedValue(mockCsvData);
574
-
575
- mockClient.executeMutation.mockResolvedValue({
576
- createProduct: { id: "123" },
577
- });
578
-
579
- // Create verbose mapper to test the logging
580
- const verboseMapper = new DataMapper(
581
- mockClient,
582
- testBasePath,
583
- mockMetrics,
584
- true
585
- );
586
- const consoleSpy = jest.spyOn(console, "log").mockImplementation();
587
-
588
- await verboseMapper.processEntity("configs/test/mappings/products.json");
589
-
590
- // Should keep custom scalar as string
591
- expect(mockClient.executeMutation).toHaveBeenCalledWith(
592
- mockMutation,
593
- {
594
- name: "Widget",
595
- custom_field: "123",
596
- },
597
- undefined
598
- );
599
-
600
- expect(consoleSpy).toHaveBeenCalledWith(
601
- 'Unknown GraphQL type "CustomScalar" for variable $custom_field. Keeping value as string.'
602
- );
603
-
604
- consoleSpy.mockRestore();
605
- });
606
- });
607
- });