@jackchuka/gql-ingest 2.0.2 → 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.
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
- }
@@ -1,211 +0,0 @@
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, false);
92
-
93
- expect(() => resolver.resolveExecutionOrder()).toThrow(
94
- "Circular dependency detected or missing dependencies for entities: a, b"
95
- );
96
- });
97
-
98
- it("should allow entities with dependencies not in the entity list when partial resolution is enabled", () => {
99
- const entities = ["a", "b"];
100
- const dependencies = {
101
- a: ["missing"],
102
- b: ["a"],
103
- };
104
- const resolver = new DependencyResolver(entities, dependencies, true);
105
-
106
- const waves = resolver.resolveExecutionOrder();
107
- expect(waves).toHaveLength(2);
108
- expect(waves[0].entities).toEqual(["a"]);
109
- expect(waves[1].entities).toEqual(["b"]);
110
- });
111
- });
112
-
113
- describe("validateDependencies", () => {
114
- it("should return no errors for valid dependencies", () => {
115
- const entities = ["users", "products", "orders"];
116
- const dependencies = {
117
- products: ["users"],
118
- orders: ["users", "products"],
119
- };
120
- const resolver = new DependencyResolver(entities, dependencies);
121
-
122
- const errors = resolver.validateDependencies();
123
-
124
- expect(errors).toHaveLength(0);
125
- });
126
-
127
- it("should detect entity with dependencies not in entity list", () => {
128
- const entities = ["users", "products"];
129
- const dependencies = {
130
- products: ["users"],
131
- orders: ["products"], // orders not in entities list
132
- };
133
- const resolver = new DependencyResolver(entities, dependencies);
134
-
135
- const errors = resolver.validateDependencies();
136
-
137
- expect(errors).toContain(
138
- "Entity 'orders' has dependencies but is not in the entity list"
139
- );
140
- });
141
-
142
- it("should detect dependencies on non-existent entities", () => {
143
- const entities = ["users", "products"];
144
- const dependencies = {
145
- products: ["users", "categories"], // categories doesn't exist
146
- };
147
- const resolver = new DependencyResolver(entities, dependencies);
148
-
149
- const errors = resolver.validateDependencies();
150
-
151
- expect(errors).toContain(
152
- "Entity 'products' depends on 'categories' which does not exist"
153
- );
154
- });
155
-
156
- it("should detect multiple validation errors", () => {
157
- const entities = ["users"];
158
- const dependencies = {
159
- products: ["categories"], // products not in list, categories doesn't exist
160
- orders: ["missing"], // orders not in list, missing doesn't exist
161
- };
162
- const resolver = new DependencyResolver(entities, dependencies);
163
-
164
- const errors = resolver.validateDependencies();
165
-
166
- expect(errors).toHaveLength(2);
167
- expect(errors).toContain(
168
- "Entity 'products' has dependencies but is not in the entity list"
169
- );
170
- expect(errors).toContain(
171
- "Entity 'orders' has dependencies but is not in the entity list"
172
- );
173
- });
174
- });
175
-
176
- describe("getDependents", () => {
177
- it("should return entities that depend on the given entity", () => {
178
- const entities = ["users", "products", "orders", "reviews"];
179
- const dependencies = {
180
- products: ["users"],
181
- orders: ["users", "products"],
182
- reviews: ["users", "products"],
183
- };
184
- const resolver = new DependencyResolver(entities, dependencies);
185
-
186
- const usersDependents = resolver.getDependents("users");
187
- const productsDependents = resolver.getDependents("products");
188
- const ordersDependents = resolver.getDependents("orders");
189
-
190
- expect(usersDependents.sort()).toEqual(["orders", "products", "reviews"]);
191
- expect(productsDependents.sort()).toEqual(["orders", "reviews"]);
192
- expect(ordersDependents).toEqual([]);
193
- });
194
- });
195
-
196
- describe("getDependencies", () => {
197
- it("should return dependencies for the given entity", () => {
198
- const entities = ["users", "products", "orders"];
199
- const dependencies = {
200
- products: ["users"],
201
- orders: ["users", "products"],
202
- };
203
- const resolver = new DependencyResolver(entities, dependencies);
204
-
205
- expect(resolver.getDependencies("users")).toEqual([]);
206
- expect(resolver.getDependencies("products")).toEqual(["users"]);
207
- expect(resolver.getDependencies("orders")).toEqual(["users", "products"]);
208
- expect(resolver.getDependencies("nonexistent")).toEqual([]);
209
- });
210
- });
211
- });
@@ -1,102 +0,0 @@
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
- private allowPartialResolution: boolean;
14
-
15
- constructor(entities: string[], dependencies: DependencyGraph = {}, allowPartialResolution: boolean = false) {
16
- this.entities = entities;
17
- this.dependencies = dependencies;
18
- this.allowPartialResolution = allowPartialResolution;
19
- }
20
-
21
- resolveExecutionOrder(): ExecutionWave[] {
22
- const waves: ExecutionWave[] = [];
23
- const processed = new Set<string>();
24
- const inProgress = new Set<string>();
25
- let waveNumber = 0;
26
-
27
- while (processed.size < this.entities.length) {
28
- const currentWave: string[] = [];
29
-
30
- for (const entity of this.entities) {
31
- if (processed.has(entity) || inProgress.has(entity)) {
32
- continue;
33
- }
34
-
35
- const deps = this.dependencies[entity] || [];
36
- const canProcess = deps.every((dep) =>
37
- processed.has(dep) || (this.allowPartialResolution && !this.entities.includes(dep))
38
- );
39
-
40
- if (canProcess) {
41
- currentWave.push(entity);
42
- inProgress.add(entity);
43
- }
44
- }
45
-
46
- if (currentWave.length === 0) {
47
- const remaining = this.entities.filter((e) => !processed.has(e));
48
- throw new Error(
49
- `Circular dependency detected or missing dependencies for entities: ${remaining.join(
50
- ", "
51
- )}`
52
- );
53
- }
54
-
55
- waves.push({
56
- entities: currentWave,
57
- wave: waveNumber++,
58
- });
59
-
60
- currentWave.forEach((entity) => {
61
- processed.add(entity);
62
- inProgress.delete(entity);
63
- });
64
- }
65
-
66
- return waves;
67
- }
68
-
69
- validateDependencies(): string[] {
70
- const errors: string[] = [];
71
- const entitySet = new Set(this.entities);
72
-
73
- for (const [entity, deps] of Object.entries(this.dependencies)) {
74
- if (!entitySet.has(entity)) {
75
- errors.push(
76
- `Entity '${entity}' has dependencies but is not in the entity list`
77
- );
78
- continue;
79
- }
80
-
81
- for (const dep of deps) {
82
- if (!entitySet.has(dep)) {
83
- errors.push(
84
- `Entity '${entity}' depends on '${dep}' which does not exist`
85
- );
86
- }
87
- }
88
- }
89
-
90
- return errors;
91
- }
92
-
93
- getDependents(entityName: string): string[] {
94
- return Object.entries(this.dependencies)
95
- .filter(([_, deps]) => deps.includes(entityName))
96
- .map(([entity, _]) => entity);
97
- }
98
-
99
- getDependencies(entityName: string): string[] {
100
- return this.dependencies[entityName] || [];
101
- }
102
- }
@@ -1,219 +0,0 @@
1
- import { GraphQLClientWrapper } from "./graphql-client";
2
-
3
- const mockRequest = jest.fn();
4
- const mockSetHeaders = jest.fn();
5
-
6
- jest.mock("graphql-request", () => ({
7
- GraphQLClient: jest.fn().mockImplementation(() => ({
8
- request: mockRequest,
9
- setHeaders: mockSetHeaders,
10
- })),
11
- }));
12
-
13
- describe("GraphQLClientWrapper", () => {
14
- let clientWrapper: GraphQLClientWrapper;
15
-
16
- beforeEach(() => {
17
- jest.clearAllMocks();
18
- clientWrapper = new GraphQLClientWrapper("https://api.example.com/graphql");
19
- });
20
-
21
- it("should create GraphQLClient with endpoint and default headers", () => {
22
- const { GraphQLClient } = require("graphql-request");
23
- expect(GraphQLClient).toHaveBeenCalledWith(
24
- "https://api.example.com/graphql",
25
- { headers: {} }
26
- );
27
- });
28
-
29
- it("should create GraphQLClient with custom headers", () => {
30
- const headers = { Authorization: "Bearer token123" };
31
- new GraphQLClientWrapper("https://api.example.com/graphql", headers);
32
-
33
- const { GraphQLClient } = require("graphql-request");
34
- expect(GraphQLClient).toHaveBeenCalledWith(
35
- "https://api.example.com/graphql",
36
- { headers }
37
- );
38
- });
39
-
40
- it("should execute mutation successfully", async () => {
41
- const mutation = "mutation { createUser(name: $name) { id } }";
42
- const variables = { name: "John" };
43
- const expectedResult = { createUser: { id: "123" } };
44
-
45
- mockRequest.mockResolvedValue(expectedResult);
46
-
47
- const result = await clientWrapper.executeMutation(mutation, variables);
48
-
49
- expect(mockRequest).toHaveBeenCalledWith(mutation, variables);
50
- expect(result).toEqual(expectedResult);
51
- });
52
-
53
- it("should handle GraphQL errors", async () => {
54
- const mutation = "mutation { createUser(name: $name) { id } }";
55
- const variables = { name: "John" };
56
- const error = new Error("GraphQL error");
57
-
58
- mockRequest.mockRejectedValue(error);
59
-
60
- const consoleSpy = jest.spyOn(console, "error").mockImplementation();
61
-
62
- await expect(
63
- clientWrapper.executeMutation(mutation, variables)
64
- ).rejects.toThrow("GraphQL error");
65
-
66
- expect(consoleSpy).toHaveBeenCalledWith(
67
- "GraphQL mutation failed after 3 attempts:",
68
- error
69
- );
70
-
71
- consoleSpy.mockRestore();
72
- });
73
-
74
- it("should set headers on the client", () => {
75
- const newHeaders = { "X-API-Key": "api123" };
76
-
77
- clientWrapper.setHeaders(newHeaders);
78
-
79
- expect(mockSetHeaders).toHaveBeenCalledWith(newHeaders);
80
- });
81
-
82
- describe("retry functionality", () => {
83
- it("should retry on retryable status codes", async () => {
84
- const mutation = "mutation { createUser(name: $name) { id } }";
85
- const variables = { name: "John" };
86
- const retryConfig = {
87
- maxAttempts: 3,
88
- baseDelay: 100,
89
- maxDelay: 1000,
90
- exponentialBackoff: false,
91
- retryableStatusCodes: [500],
92
- };
93
-
94
- const serverError = new Error("Server Error");
95
- (serverError as any).response = { status: 500 };
96
-
97
- mockRequest
98
- .mockRejectedValueOnce(serverError)
99
- .mockRejectedValueOnce(serverError)
100
- .mockResolvedValueOnce({ data: { result: "success" } });
101
-
102
- const result = await clientWrapper.executeMutation(
103
- mutation,
104
- variables,
105
- retryConfig
106
- );
107
-
108
- expect(mockRequest).toHaveBeenCalledTimes(3);
109
- expect(result).toEqual({ data: { result: "success" } });
110
- });
111
-
112
- it("should not retry on non-retryable status codes", async () => {
113
- const mutation = "mutation { createUser(name: $name) { id } }";
114
- const variables = { name: "John" };
115
- const retryConfig = {
116
- maxAttempts: 3,
117
- baseDelay: 100,
118
- maxDelay: 1000,
119
- exponentialBackoff: false,
120
- retryableStatusCodes: [500],
121
- };
122
-
123
- const clientError = new Error("Bad Request");
124
- (clientError as any).response = { status: 400 };
125
-
126
- mockRequest.mockRejectedValueOnce(clientError);
127
-
128
- await expect(
129
- clientWrapper.executeMutation(mutation, variables, retryConfig)
130
- ).rejects.toThrow("Bad Request");
131
-
132
- expect(mockRequest).toHaveBeenCalledTimes(1);
133
- });
134
-
135
- it("should retry on network errors (no response)", async () => {
136
- const mutation = "mutation { createUser(name: $name) { id } }";
137
- const variables = { name: "John" };
138
- const retryConfig = {
139
- maxAttempts: 2,
140
- baseDelay: 100,
141
- maxDelay: 1000,
142
- exponentialBackoff: false,
143
- retryableStatusCodes: [500],
144
- };
145
-
146
- const networkError = new Error("Network Error");
147
- // No response property = network error
148
-
149
- mockRequest
150
- .mockRejectedValueOnce(networkError)
151
- .mockResolvedValueOnce({ data: { result: "success" } });
152
-
153
- const result = await clientWrapper.executeMutation(
154
- mutation,
155
- variables,
156
- retryConfig
157
- );
158
-
159
- expect(mockRequest).toHaveBeenCalledTimes(2);
160
- expect(result).toEqual({ data: { result: "success" } });
161
- });
162
-
163
- it("should fail after max attempts are exhausted", async () => {
164
- const mutation = "mutation { createUser(name: $name) { id } }";
165
- const variables = { name: "John" };
166
- const retryConfig = {
167
- maxAttempts: 2,
168
- baseDelay: 100,
169
- maxDelay: 1000,
170
- exponentialBackoff: false,
171
- retryableStatusCodes: [500],
172
- };
173
-
174
- const serverError = new Error("Server Error");
175
- (serverError as any).response = { status: 500 };
176
-
177
- mockRequest.mockRejectedValue(serverError);
178
-
179
- await expect(
180
- clientWrapper.executeMutation(mutation, variables, retryConfig)
181
- ).rejects.toThrow("Server Error");
182
-
183
- expect(mockRequest).toHaveBeenCalledTimes(2);
184
- });
185
-
186
- it("should use exponential backoff when configured", async () => {
187
- const mutation = "mutation { createUser(name: $name) { id } }";
188
- const variables = { name: "John" };
189
- const retryConfig = {
190
- maxAttempts: 3,
191
- baseDelay: 100,
192
- maxDelay: 1000,
193
- exponentialBackoff: true,
194
- retryableStatusCodes: [500],
195
- };
196
-
197
- const serverError = new Error("Server Error");
198
- (serverError as any).response = { status: 500 };
199
-
200
- mockRequest
201
- .mockRejectedValueOnce(serverError)
202
- .mockRejectedValueOnce(serverError)
203
- .mockResolvedValueOnce({ data: { result: "success" } });
204
-
205
- const startTime = Date.now();
206
- const result = await clientWrapper.executeMutation(
207
- mutation,
208
- variables,
209
- retryConfig
210
- );
211
- const totalTime = Date.now() - startTime;
212
-
213
- expect(mockRequest).toHaveBeenCalledTimes(3);
214
- expect(result).toEqual({ data: { result: "success" } });
215
- // Should have some delay for retries (base + exponential)
216
- expect(totalTime).toBeGreaterThan(200); // At least 100ms base + 200ms exponential
217
- });
218
- });
219
- });