@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.
@@ -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
- });
@@ -1,151 +0,0 @@
1
- import { GraphQLClient } from "graphql-request";
2
- import { MetricsCollector } from "./metrics";
3
- import { DEFAULT_RETRY_CONFIG, RetryConfig } from "./config";
4
-
5
- export class GraphQLClientWrapper {
6
- private client: GraphQLClient;
7
- private metrics?: MetricsCollector;
8
- private verbose: boolean;
9
-
10
- constructor(
11
- endpoint: string,
12
- headers?: Record<string, string>,
13
- metrics?: MetricsCollector,
14
- verbose: boolean = false
15
- ) {
16
- this.client = new GraphQLClient(endpoint, {
17
- headers: headers || {},
18
- });
19
- this.metrics = metrics;
20
- this.verbose = verbose;
21
- }
22
-
23
- async executeMutation(
24
- mutation: string,
25
- variables: Record<string, any>,
26
- retryConfig?: RetryConfig
27
- ): Promise<any> {
28
- const config = retryConfig || DEFAULT_RETRY_CONFIG;
29
-
30
- let lastError: any;
31
- const totalStartTime = Date.now();
32
-
33
- for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
34
- const attemptStartTime = Date.now();
35
-
36
- try {
37
- const result = await this.client.request(mutation, variables);
38
-
39
- if (this.metrics) {
40
- const duration = Date.now() - attemptStartTime;
41
- this.metrics.recordRequestDuration(duration);
42
- if (attempt > 0) {
43
- this.metrics.recordRetrySuccess(attempt);
44
- }
45
- }
46
-
47
- if (this.verbose) {
48
- const totalDuration = Date.now() - totalStartTime;
49
- const retryInfo =
50
- attempt > 0 ? ` (succeeded on attempt ${attempt + 1})` : "";
51
- console.log(
52
- `✓ GraphQL request completed in ${totalDuration}ms${retryInfo}:`,
53
- result
54
- );
55
- }
56
-
57
- return result;
58
- } catch (error: any) {
59
- lastError = error;
60
- const duration = Date.now() - attemptStartTime;
61
-
62
- if (this.metrics) {
63
- this.metrics.recordRequestDuration(duration);
64
- }
65
-
66
- // Check if this is the last attempt
67
- if (attempt === config.maxAttempts - 1) {
68
- if (this.metrics && attempt > 0) {
69
- this.metrics.recordRetryFailure(attempt);
70
- }
71
- break;
72
- }
73
-
74
- // Check if error is retryable
75
- if (!this.isRetryableError(error, config)) {
76
- if (this.verbose) {
77
- console.error(
78
- `✗ GraphQL request failed with non-retryable error in ${duration}ms:`,
79
- error
80
- );
81
- } else {
82
- console.error("GraphQL mutation failed (non-retryable):", error);
83
- }
84
- throw error;
85
- }
86
-
87
- // Calculate delay
88
- const delay = this.calculateDelay(attempt, config);
89
-
90
- if (this.verbose) {
91
- console.log(
92
- `⏳ GraphQL request failed (attempt ${attempt + 1}/${
93
- config.maxAttempts
94
- }), retrying in ${delay}ms...`
95
- );
96
- }
97
-
98
- // Wait before retry
99
- await this.sleep(delay);
100
- }
101
- }
102
-
103
- // All retries exhausted
104
- if (this.verbose) {
105
- const totalDuration = Date.now() - totalStartTime;
106
- console.error(
107
- `✗ GraphQL request failed after ${config.maxAttempts} attempts in ${totalDuration}ms:`,
108
- lastError
109
- );
110
- } else {
111
- console.error(
112
- `GraphQL mutation failed after ${config.maxAttempts} attempts:`,
113
- lastError
114
- );
115
- }
116
-
117
- throw lastError;
118
- }
119
-
120
- private isRetryableError(error: any, config: RetryConfig): boolean {
121
- // Network errors (no response)
122
- if (!error.response) {
123
- return true;
124
- }
125
-
126
- // Check HTTP status codes
127
- const status = error.response.status;
128
- return config.retryableStatusCodes.includes(status);
129
- }
130
-
131
- private calculateDelay(attempt: number, config: RetryConfig): number {
132
- if (!config.exponentialBackoff) {
133
- return config.baseDelay;
134
- }
135
-
136
- const exponentialDelay = config.baseDelay * Math.pow(2, attempt);
137
- const cappedDelay = Math.min(exponentialDelay, config.maxDelay);
138
-
139
- // Add jitter (±20% randomization)
140
- const jitter = cappedDelay * 0.2 * (Math.random() - 0.5);
141
- return Math.max(0, cappedDelay + jitter);
142
- }
143
-
144
- private sleep(ms: number): Promise<void> {
145
- return new Promise((resolve) => setTimeout(resolve, ms));
146
- }
147
-
148
- setHeaders(headers: Record<string, string>) {
149
- this.client.setHeaders(headers);
150
- }
151
- }