@codex-data/sdk 1.0.9 → 1.0.10

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.
Files changed (38) hide show
  1. package/dist/resources/graphql.d.ts +3 -1
  2. package/dist/resources/graphql.d.ts.map +1 -1
  3. package/dist/resources/graphql.js +2 -0
  4. package/dist/resources/graphql.js.map +1 -1
  5. package/dist/sdk/Mutation.d.ts +2 -2
  6. package/dist/sdk/Mutation.d.ts.map +1 -1
  7. package/dist/sdk/Mutation.js +2 -2
  8. package/dist/sdk/Mutation.js.map +1 -1
  9. package/dist/sdk/Query.d.ts +51 -51
  10. package/dist/sdk/Query.d.ts.map +1 -1
  11. package/dist/sdk/Query.js +51 -51
  12. package/dist/sdk/Query.js.map +1 -1
  13. package/dist/sdk/Subscribe.d.ts +17 -17
  14. package/dist/sdk/Subscribe.d.ts.map +1 -1
  15. package/dist/sdk/Subscribe.js +129 -129
  16. package/dist/sdk/Subscribe.js.map +1 -1
  17. package/dist/sdk/generated/gql.d.ts +0 -3
  18. package/dist/sdk/generated/gql.d.ts.map +1 -1
  19. package/dist/sdk/generated/graphql.d.ts +3 -1
  20. package/dist/sdk/generated/graphql.d.ts.map +1 -1
  21. package/dist/sdk/generated/graphql.js +2 -0
  22. package/dist/sdk/generated/graphql.js.map +1 -1
  23. package/dist/sdk/index.d.ts +45 -0
  24. package/dist/sdk/index.d.ts.map +1 -1
  25. package/dist/sdk/index.js +83 -5
  26. package/dist/sdk/index.js.map +1 -1
  27. package/dist/sdk/index.test.js +232 -7
  28. package/dist/sdk/index.test.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/resources/graphql.schema.json +13 -1
  31. package/src/resources/graphql.ts +3 -1
  32. package/src/resources/schema.graphql +3 -1
  33. package/src/sdk/Mutation.ts +4 -4
  34. package/src/sdk/Query.ts +107 -107
  35. package/src/sdk/Subscribe.ts +217 -217
  36. package/src/sdk/generated/graphql.ts +3 -1
  37. package/src/sdk/index.test.ts +361 -10
  38. package/src/sdk/index.ts +89 -6
@@ -1,6 +1,6 @@
1
1
  import { DocumentNode } from "graphql";
2
2
 
3
- import { Codex } from "./index";
3
+ import { ApiConfig, Codex } from "./index";
4
4
 
5
5
  const getNetworksDocument = {
6
6
  kind: "Document",
@@ -34,45 +34,396 @@ const subscribePriceUpdatedString = `subscription onPriceUpdated($address: Strin
34
34
 
35
35
  describe("Codex", () => {
36
36
  let sdk: Codex;
37
+
37
38
  beforeEach(() => {
38
39
  sdk = new Codex("dummy-key");
39
40
  });
40
41
 
42
+ describe("constructor", () => {
43
+ it("should initialize with default config when no apiConfig provided", () => {
44
+ const sdkInstance = new Codex("test-key");
45
+ expect(sdkInstance).toBeInstanceOf(Codex);
46
+ expect(sdkInstance.queries).toBeDefined();
47
+ expect(sdkInstance.mutations).toBeDefined();
48
+ expect(sdkInstance.subscriptions).toBeDefined();
49
+ });
50
+
51
+ it("should throw error when apiKey is not provided", () => {
52
+ expect(() => new Codex("")).toThrow("apiKey must be defined");
53
+ expect(() => new Codex(null as any)).toThrow("apiKey must be defined");
54
+ expect(() => new Codex(undefined as any)).toThrow(
55
+ "apiKey must be defined",
56
+ );
57
+ });
58
+
59
+ it("should initialize with custom apiConfig", () => {
60
+ const customConfig: Partial<ApiConfig> = {
61
+ apiUrl: "https://custom.api.com/graphql",
62
+ apiRealtimeUrl: "wss://custom.api.com/graphql",
63
+ headers: { "Custom-Header": "value" },
64
+ wsHeaders: { "WS-Header": "ws-value" },
65
+ ws: false,
66
+ };
67
+
68
+ const sdkInstance = new Codex("test-key", customConfig);
69
+ expect(sdkInstance).toBeInstanceOf(Codex);
70
+ });
71
+
72
+ it("should merge custom config with default config", () => {
73
+ const customConfig: Partial<ApiConfig> = {
74
+ headers: { "Custom-Header": "value" },
75
+ ws: false,
76
+ };
77
+
78
+ // Mock the createGraphQLClient and createWebsocketClient to verify they're called with merged config
79
+ const createGraphQLClientSpy = jest.spyOn(
80
+ Codex.prototype as any,
81
+ "createGraphQLClient",
82
+ );
83
+ const createWebsocketClientSpy = jest.spyOn(
84
+ Codex.prototype as any,
85
+ "createWebsocketClient",
86
+ );
87
+
88
+ new Codex("test-key", customConfig);
89
+
90
+ // Verify the merged config was used
91
+ expect(createGraphQLClientSpy).toHaveBeenCalledWith(
92
+ expect.objectContaining({
93
+ apiUrl: "https://graph.codex.io/graphql", // default
94
+ apiRealtimeUrl: "wss://graph.codex.io/graphql", // default
95
+ headers: { "Custom-Header": "value" }, // custom
96
+ ws: false, // custom
97
+ }),
98
+ );
99
+
100
+ createGraphQLClientSpy.mockRestore();
101
+ createWebsocketClientSpy.mockRestore();
102
+ });
103
+ });
104
+
105
+ describe("updateConfig", () => {
106
+ it("should update configuration and recreate clients", () => {
107
+ const disposeSpy = jest.spyOn(sdk, "dispose");
108
+ const createGraphQLClientSpy = jest.spyOn(
109
+ sdk as any,
110
+ "createGraphQLClient",
111
+ );
112
+ const createWebsocketClientSpy = jest.spyOn(
113
+ sdk as any,
114
+ "createWebsocketClient",
115
+ );
116
+
117
+ const newConfig: Partial<ApiConfig> = {
118
+ apiUrl: "https://new-api.com/graphql",
119
+ headers: { "New-Header": "new-value" },
120
+ };
121
+
122
+ sdk.updateConfig(newConfig);
123
+
124
+ expect(disposeSpy).toHaveBeenCalled();
125
+ expect(createGraphQLClientSpy).toHaveBeenCalled();
126
+ expect(createWebsocketClientSpy).toHaveBeenCalled();
127
+
128
+ disposeSpy.mockRestore();
129
+ createGraphQLClientSpy.mockRestore();
130
+ createWebsocketClientSpy.mockRestore();
131
+ });
132
+
133
+ it("should merge new config with existing config", () => {
134
+ const initialConfig: Partial<ApiConfig> = {
135
+ headers: { "Initial-Header": "initial-value" },
136
+ ws: true,
137
+ };
138
+
139
+ const sdkWithConfig = new Codex("test-key", initialConfig);
140
+ const parseConfigSpy = jest.spyOn(sdkWithConfig as any, "parseConfig");
141
+
142
+ const updateConfig: Partial<ApiConfig> = {
143
+ headers: { "Updated-Header": "updated-value" },
144
+ apiUrl: "https://updated-api.com/graphql",
145
+ };
146
+
147
+ sdkWithConfig.updateConfig(updateConfig);
148
+
149
+ // Verify parseConfig was called with merged configuration
150
+ expect(parseConfigSpy).toHaveBeenCalledWith(
151
+ expect.objectContaining({
152
+ headers: { "Updated-Header": "updated-value" },
153
+ apiUrl: "https://updated-api.com/graphql",
154
+ ws: true, // from initial config
155
+ }),
156
+ );
157
+
158
+ parseConfigSpy.mockRestore();
159
+ });
160
+
161
+ it("should dispose existing websocket connection before creating new one", () => {
162
+ // Mock wsClient
163
+ const mockDispose = jest.fn();
164
+ (sdk as any).wsClient = { dispose: mockDispose };
165
+
166
+ const disposeSpy = jest.spyOn(sdk, "dispose");
167
+
168
+ sdk.updateConfig({ headers: { "New-Header": "value" } });
169
+
170
+ expect(disposeSpy).toHaveBeenCalled();
171
+ disposeSpy.mockRestore();
172
+ });
173
+ });
174
+
175
+ describe("getRequestHeaders", () => {
176
+ it("should return static headers when no dynamic headers are configured", async () => {
177
+ const staticHeaders = { "Static-Header": "static-value" };
178
+ const sdkWithHeaders = new Codex("test-key", { headers: staticHeaders });
179
+
180
+ const headers = await (sdkWithHeaders as any).getRequestHeaders();
181
+
182
+ expect(headers).toEqual(staticHeaders);
183
+ });
184
+
185
+ it("should return dynamic headers when applyHeaders is configured", async () => {
186
+ const dynamicHeaders = { "Dynamic-Header": "dynamic-value" };
187
+ const applyHeaders = jest.fn().mockResolvedValue(dynamicHeaders);
188
+
189
+ const sdkWithDynamicHeaders = new Codex("test-key", { applyHeaders });
190
+
191
+ const headers = await (sdkWithDynamicHeaders as any).getRequestHeaders();
192
+
193
+ expect(applyHeaders).toHaveBeenCalled();
194
+ expect(headers).toEqual(dynamicHeaders);
195
+ });
196
+
197
+ it("should merge static and dynamic headers with dynamic taking precedence", async () => {
198
+ const staticHeaders = { "Header-1": "static", "Header-2": "static" };
199
+ const dynamicHeaders = { "Header-2": "dynamic", "Header-3": "dynamic" };
200
+ const applyHeaders = jest.fn().mockResolvedValue(dynamicHeaders);
201
+
202
+ const sdkWithBothHeaders = new Codex("test-key", {
203
+ headers: staticHeaders,
204
+ applyHeaders,
205
+ });
206
+
207
+ const headers = await (sdkWithBothHeaders as any).getRequestHeaders();
208
+
209
+ expect(headers).toEqual({
210
+ "Header-1": "static",
211
+ "Header-2": "dynamic", // dynamic overrides static
212
+ "Header-3": "dynamic",
213
+ });
214
+ });
215
+
216
+ it("should handle applyHeaders returning undefined", async () => {
217
+ const staticHeaders = { "Static-Header": "static-value" };
218
+ const applyHeaders = jest.fn().mockResolvedValue(undefined);
219
+
220
+ const sdkWithUndefinedDynamic = new Codex("test-key", {
221
+ headers: staticHeaders,
222
+ applyHeaders,
223
+ });
224
+
225
+ const headers = await (
226
+ sdkWithUndefinedDynamic as any
227
+ ).getRequestHeaders();
228
+
229
+ expect(headers).toEqual(staticHeaders);
230
+ });
231
+ });
232
+
233
+ describe("dispose", () => {
234
+ it("should dispose websocket client when it exists", async () => {
235
+ const mockDispose = jest.fn();
236
+ (sdk as any).wsClient = { dispose: mockDispose };
237
+
238
+ await sdk.dispose();
239
+
240
+ expect(mockDispose).toHaveBeenCalled();
241
+ });
242
+
243
+ it("should not throw error when websocket client is undefined", async () => {
244
+ (sdk as any).wsClient = undefined;
245
+
246
+ await expect(sdk.dispose()).resolves.not.toThrow();
247
+ });
248
+ });
249
+
250
+ describe("websocket configuration", () => {
251
+ it("should create websocket client when ws is true (default)", () => {
252
+ const createWebsocketClientSpy = jest.spyOn(
253
+ Codex.prototype as any,
254
+ "createWebsocketClient",
255
+ );
256
+
257
+ new Codex("test-key");
258
+
259
+ expect(createWebsocketClientSpy).toHaveBeenCalledWith(
260
+ expect.objectContaining({ ws: true }),
261
+ );
262
+
263
+ createWebsocketClientSpy.mockRestore();
264
+ });
265
+
266
+ it("should not create websocket client when ws is false", () => {
267
+ const createWebsocketClientSpy = jest.spyOn(
268
+ Codex.prototype as any,
269
+ "createWebsocketClient",
270
+ );
271
+
272
+ new Codex("test-key", { ws: false });
273
+
274
+ expect(createWebsocketClientSpy).toHaveBeenCalledWith(
275
+ expect.objectContaining({ ws: false }),
276
+ );
277
+
278
+ createWebsocketClientSpy.mockRestore();
279
+ });
280
+
281
+ it("should throw error when trying to subscribe without websocket enabled", () => {
282
+ const sdkWithoutWs = new Codex("test-key", { ws: false });
283
+
284
+ expect(() => {
285
+ sdkWithoutWs.subscribe(
286
+ subscribePriceUpdatedString,
287
+ { address: "0xtoken", networkId: 1 },
288
+ { next: jest.fn(), error: jest.fn(), complete: jest.fn() },
289
+ );
290
+ }).toThrow("Websocket is not enabled in the config");
291
+ });
292
+ });
293
+
41
294
  describe("query", () => {
42
- it("send a fetch request with the correct params", async () => {
295
+ it("should send a fetch request with the correct params", async () => {
43
296
  const fetchSpy = jest
44
297
  .spyOn(sdk["client"], "request")
45
298
  .mockResolvedValue({} as unknown);
299
+
46
300
  await sdk.query(getNetworksDocument, {});
301
+
47
302
  expect(fetchSpy).toHaveBeenCalledWith(getNetworksDocument, {}, {});
48
303
  });
304
+
305
+ it("should include request headers in the request", async () => {
306
+ const mockHeaders = { "Test-Header": "test-value" };
307
+ const getRequestHeadersSpy = jest
308
+ .spyOn(sdk as any, "getRequestHeaders")
309
+ .mockResolvedValue(mockHeaders);
310
+ const fetchSpy = jest
311
+ .spyOn(sdk["client"], "request")
312
+ .mockResolvedValue({} as unknown);
313
+
314
+ await sdk.query(getNetworksDocument, {});
315
+
316
+ expect(getRequestHeadersSpy).toHaveBeenCalled();
317
+ expect(fetchSpy).toHaveBeenCalledWith(
318
+ getNetworksDocument,
319
+ {},
320
+ mockHeaders,
321
+ );
322
+
323
+ getRequestHeadersSpy.mockRestore();
324
+ });
49
325
  });
326
+
327
+ describe("mutation", () => {
328
+ it("should send a request with the correct params", async () => {
329
+ const fetchSpy = jest
330
+ .spyOn(sdk["client"], "request")
331
+ .mockResolvedValue({} as unknown);
332
+
333
+ await sdk.mutation(getNetworksDocument, {});
334
+
335
+ expect(fetchSpy).toHaveBeenCalledWith(getNetworksDocument, {}, {});
336
+ });
337
+
338
+ it("should include request headers in the request", async () => {
339
+ const mockHeaders = { "Test-Header": "test-value" };
340
+ const getRequestHeadersSpy = jest
341
+ .spyOn(sdk as any, "getRequestHeaders")
342
+ .mockResolvedValue(mockHeaders);
343
+ const fetchSpy = jest
344
+ .spyOn(sdk["client"], "request")
345
+ .mockResolvedValue({} as unknown);
346
+
347
+ await sdk.mutation(getNetworksDocument, {});
348
+
349
+ expect(getRequestHeadersSpy).toHaveBeenCalled();
350
+ expect(fetchSpy).toHaveBeenCalledWith(
351
+ getNetworksDocument,
352
+ {},
353
+ mockHeaders,
354
+ );
355
+
356
+ getRequestHeadersSpy.mockRestore();
357
+ });
358
+ });
359
+
50
360
  describe("send", () => {
51
361
  it("should return the results of the query", async () => {
52
362
  const fetchSpy = jest
53
363
  .spyOn(sdk["client"], "request")
54
364
  .mockResolvedValue({} as unknown);
365
+
55
366
  await sdk.send(getNetworksString, {});
367
+
56
368
  expect(fetchSpy).toHaveBeenCalledWith(getNetworksString, {}, {});
57
369
  });
370
+
371
+ it("should include request headers in the request", async () => {
372
+ const mockHeaders = { "Test-Header": "test-value" };
373
+ const getRequestHeadersSpy = jest
374
+ .spyOn(sdk as any, "getRequestHeaders")
375
+ .mockResolvedValue(mockHeaders);
376
+ const fetchSpy = jest
377
+ .spyOn(sdk["client"], "request")
378
+ .mockResolvedValue({} as unknown);
379
+
380
+ await sdk.send(getNetworksString, {});
381
+
382
+ expect(getRequestHeadersSpy).toHaveBeenCalled();
383
+ expect(fetchSpy).toHaveBeenCalledWith(getNetworksString, {}, mockHeaders);
384
+
385
+ getRequestHeadersSpy.mockRestore();
386
+ });
58
387
  });
388
+
59
389
  describe("subscribe", () => {
60
390
  it("should return a cleanup function", () => {
61
- const next = jest.fn(),
62
- error = jest.fn(),
63
- complete = jest.fn();
64
- const sink = {
65
- next,
66
- error,
67
- complete,
68
- };
391
+ const next = jest.fn();
392
+ const error = jest.fn();
393
+ const complete = jest.fn();
394
+ const sink = { next, error, complete };
395
+
69
396
  jest.spyOn(sdk["wsClient"] as any, "subscribe").mockReturnValue(() => {});
397
+
70
398
  const cleanup = sdk.subscribe(
71
399
  subscribePriceUpdatedString,
72
400
  { address: "0xtoken", networkId: 1 },
73
401
  sink,
74
402
  );
403
+
75
404
  expect(typeof cleanup).toBe("function");
76
405
  });
406
+
407
+ it("should call wsClient.subscribe with correct parameters", () => {
408
+ const next = jest.fn();
409
+ const error = jest.fn();
410
+ const complete = jest.fn();
411
+ const sink = { next, error, complete };
412
+ const variables = { address: "0xtoken", networkId: 1 };
413
+
414
+ const subscribeSpy = jest
415
+ .spyOn(sdk["wsClient"] as any, "subscribe")
416
+ .mockReturnValue(() => {});
417
+
418
+ sdk.subscribe(subscribePriceUpdatedString, variables, sink);
419
+
420
+ expect(subscribeSpy).toHaveBeenCalledWith(
421
+ {
422
+ query: subscribePriceUpdatedString,
423
+ variables,
424
+ },
425
+ sink,
426
+ );
427
+ });
77
428
  });
78
429
  });
package/src/sdk/index.ts CHANGED
@@ -48,17 +48,34 @@ export class Codex {
48
48
  public mutations: Mutation;
49
49
  public subscriptions: Subscribe;
50
50
 
51
+ /**
52
+ * @param apiKey - The api key to use for the requests.
53
+ * @param apiConfig - The configuration to use for the requests.
54
+ */
51
55
  constructor(
52
56
  private apiKey: string,
53
57
  private apiConfig?: Partial<ApiConfig>,
54
58
  ) {
55
- invariant(this.apiKey, "apiKey must be defined");
56
- const config = Object.assign({}, defaultConfig, apiConfig);
57
-
59
+ const config = this.parseConfig(this.apiConfig ?? {});
58
60
  this.queries = new Query(this);
59
61
  this.mutations = new Mutation(this);
60
62
  this.subscriptions = new Subscribe(this);
61
- this.client = new GraphQLClient(config.apiUrl, {
63
+
64
+ this.client = this.createGraphQLClient(config);
65
+ this.wsClient = this.createWebsocketClient(config);
66
+
67
+ this.wsClient?.on("error", (error) => {
68
+ console.error("Websocket error: ", error);
69
+ });
70
+ }
71
+
72
+ private parseConfig(config: Partial<ApiConfig>) {
73
+ invariant(this.apiKey, "apiKey must be defined");
74
+ return Object.assign({}, defaultConfig, config);
75
+ }
76
+
77
+ private createGraphQLClient(config: ApiConfig) {
78
+ return new GraphQLClient(config.apiUrl, {
62
79
  method: "POST",
63
80
  headers: new Headers({
64
81
  "Content-Type": "application/json",
@@ -67,7 +84,10 @@ export class Codex {
67
84
  ...config.headers,
68
85
  }),
69
86
  });
70
- this.wsClient = config.ws
87
+ }
88
+
89
+ private createWebsocketClient(config: ApiConfig) {
90
+ return config.ws
71
91
  ? createClient({
72
92
  webSocketImpl: WebSocket,
73
93
  keepAlive: 10_000, // ping server every 10 seconds
@@ -91,6 +111,51 @@ export class Codex {
91
111
  return headers;
92
112
  }
93
113
 
114
+ /**
115
+ * Need to update the headers on the websocket connection.
116
+ * This is useful if the api key changes, or you need to modify the connection init params at all.
117
+ * NOTE: it will dispose of the old connection, so all existing subscriptions will be closed.
118
+ */
119
+ public updateConfig(incomingConfig: Partial<ApiConfig>) {
120
+ // If we have an existing websocket connection, dispose of it.
121
+ if (this.wsClient) this.dispose();
122
+
123
+ // Merge the incoming config with the existing config.
124
+ const config = this.parseConfig(
125
+ Object.assign({}, this.apiConfig, incomingConfig),
126
+ );
127
+
128
+ // Create a new websocket connection with the new headers.
129
+ this.wsClient = this.createWebsocketClient(config);
130
+ this.client = this.createGraphQLClient(config);
131
+
132
+ this.wsClient?.on("error", (error) => {
133
+ console.error("Websocket error: ", error);
134
+ });
135
+ this.wsClient?.on("closed", (er) => {
136
+ console.log("Websocket closed", er);
137
+ });
138
+ this.wsClient?.on("connecting", () => {
139
+ console.log("Websocket connecting");
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Call this to dispose of the websocket connection, and make this class ready for garbage collection.
145
+ * This will also close any open subscriptions.
146
+ *
147
+ * It's useful for when you need to modify some of the configuration, or when you're done with the class.
148
+ */
149
+ public async dispose() {
150
+ this.wsClient?.dispose();
151
+ }
152
+
153
+ /**
154
+ * This is the preferred way to execute queries.
155
+ * @param doc - The graphql document node to execute.
156
+ * @param args - The variables to pass to the document.
157
+ * @returns The result of the query.
158
+ */
94
159
  public async query<TResults, TVars extends Variables>(
95
160
  doc: TypedDocumentNode<TResults, TVars>,
96
161
  args: TVars = {} as TVars,
@@ -103,6 +168,12 @@ export class Codex {
103
168
  return res as TResults;
104
169
  }
105
170
 
171
+ /**
172
+ * This is functionally the same thing as the `query` method, but it's more expressive.
173
+ * @param doc - The graphql document node to execute.
174
+ * @param args - The variables to pass to the document.
175
+ * @returns The result of the mutation.
176
+ */
106
177
  public async mutation<TResults, TVars extends Variables>(
107
178
  doc: TypedDocumentNode<TResults, TVars>,
108
179
  args: TVars = {} as TVars,
@@ -115,7 +186,13 @@ export class Codex {
115
186
  return res as TResults;
116
187
  }
117
188
 
118
- // Very simple network based fetch implementation, no compilation required
189
+ /**
190
+ * This should be used when you don't want to use graphql libaries to create a document node,
191
+ * and would rather just pass in a string.
192
+ * @param gqlString - The graphql document string to execute.
193
+ * @param args - The variables to pass to the document.
194
+ * @returns The result of the mutation.
195
+ */
119
196
  public async send<TResults, V extends Variables = Variables>(
120
197
  gqlString: string,
121
198
  args: V = {} as V,
@@ -128,6 +205,12 @@ export class Codex {
128
205
  return res;
129
206
  }
130
207
 
208
+ /**
209
+ * @param doc - The graphql document string to execute.
210
+ * @param args - The variables to pass to the document.
211
+ * @param sink - The sink to receive the results.
212
+ * @returns A cleanup function to unsubscribe from the subscription.
213
+ */
131
214
  public subscribe<
132
215
  TResults,
133
216
  TVars extends Record<string, unknown> = Record<string, never>,