Skip to content

Commit f8441e6

Browse files
Add transformFetchOptions strategy, called just prior to fetch
Whereas existing `makeFetchOptions` strategy makes the initial fetch options and is probably provided by the db `makeHTTPClient`, the `transformFetchOptions` strategy transforms the fetch options at the last possible point before sending the request, and can be used to add/remove headers, add an abort signal, change the URL, etc
1 parent 2274546 commit f8441e6

File tree

11 files changed

+185
-36
lines changed

11 files changed

+185
-36
lines changed

packages/client-http/client-http.test.ts

Lines changed: 144 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach } from "vitest";
22
import { makeClient } from "./client-http";
33
import { makeAuthHeaders } from "@madatdata/base-client";
44
import { setupMswServerTestHooks } from "@madatdata/test-helpers/msw-server-hooks";
5+
import { setupMemo } from "@madatdata/test-helpers/setup-memo";
56
import { shouldSkipIntegrationTests } from "@madatdata/test-helpers/env-config";
67
import { rest } from "msw";
78

@@ -10,8 +11,11 @@ import type { HTTPStrategies } from "./strategies/types";
1011
import {
1112
skipParsingFieldsFromResponse,
1213
parseFieldsFromResponseBodyJSONFieldsKey,
14+
skipTransformFetchOptions,
1315
} from ".";
1416

17+
import type { IsomorphicRequest } from "@mswjs/interceptors";
18+
1519
// NOTE: Previously, the default http-client was hardcoded for Splitgraph, which
1620
// is why all the tests reflect its shape. But we don't want this package to
1721
// depend on db-splitgraph, so we copy the strategies from DbSplitgraph.makeHTTPClient
@@ -45,41 +49,42 @@ const splitgraphClientOptions = {
4549
},
4650
parseFieldsFromResponse: skipParsingFieldsFromResponse,
4751
parseFieldsFromResponseBodyJSON: parseFieldsFromResponseBodyJSONFieldsKey,
52+
transformFetchOptions: skipTransformFetchOptions,
4853
} as HTTPStrategies,
4954
};
5055

56+
const minSuccessfulJSON = {
57+
success: true,
58+
command: "SELECT",
59+
rowCount: 1,
60+
rows: [
61+
{
62+
"?column?": 1,
63+
},
64+
],
65+
fields: [
66+
{
67+
name: "?column?",
68+
tableID: 0,
69+
columnID: 0,
70+
dataTypeID: 23,
71+
dataTypeSize: 4,
72+
dataTypeModifier: -1,
73+
format: "text",
74+
formattedType: "INT4",
75+
},
76+
],
77+
executionTime: "128ms",
78+
executionTimeHighRes: "0s 128.383115ms",
79+
};
80+
5181
describe("makeClient creates client which", () => {
5282
setupMswServerTestHooks();
5383

5484
beforeEach(({ mswServer }) => {
5585
mswServer?.use(
5686
rest.post(defaultHost.baseUrls.sql + "/ddn", (_req, res, ctx) => {
57-
return res(
58-
ctx.json({
59-
success: true,
60-
command: "SELECT",
61-
rowCount: 1,
62-
rows: [
63-
{
64-
"?column?": 1,
65-
},
66-
],
67-
fields: [
68-
{
69-
name: "?column?",
70-
tableID: 0,
71-
columnID: 0,
72-
dataTypeID: 23,
73-
dataTypeSize: 4,
74-
dataTypeModifier: -1,
75-
format: "text",
76-
formattedType: "INT4",
77-
},
78-
],
79-
executionTime: "128ms",
80-
executionTimeHighRes: "0s 128.383115ms",
81-
})
82-
);
87+
return res(ctx.json(minSuccessfulJSON));
8388
})
8489
);
8590
});
@@ -125,22 +130,125 @@ describe("makeClient creates client which", () => {
125130
});
126131
});
127132

128-
const makeStubClient = () => {
133+
const stubStrategies: HTTPStrategies = {
134+
makeFetchOptions: () => ({
135+
method: "POST",
136+
}),
137+
makeQueryURL: async ({ host, database }) => {
138+
return Promise.resolve(host.baseUrls.sql + "/" + database.dbname);
139+
},
140+
parseFieldsFromResponse: skipParsingFieldsFromResponse,
141+
parseFieldsFromResponseBodyJSON: parseFieldsFromResponseBodyJSONFieldsKey,
142+
transformFetchOptions: skipTransformFetchOptions,
143+
};
144+
145+
const makeStubClient = (opts?: { strategies?: Partial<HTTPStrategies> }) => {
129146
return makeClient({
130147
credential: null,
131148
strategies: {
132-
makeFetchOptions: () => ({
133-
method: "POST",
134-
}),
135-
makeQueryURL: async ({ host, database }) => {
136-
return Promise.resolve(host.baseUrls.sql + "/" + database.dbname);
137-
},
138-
parseFieldsFromResponse: skipParsingFieldsFromResponse,
139-
parseFieldsFromResponseBodyJSON: parseFieldsFromResponseBodyJSONFieldsKey,
149+
...stubStrategies,
150+
...opts?.strategies,
140151
},
141152
});
142153
};
143154

155+
describe("client implements strategies", () => {
156+
setupMswServerTestHooks();
157+
setupMemo();
158+
159+
beforeEach((testCtx) => {
160+
const { mswServer, useTestMemo } = testCtx;
161+
162+
const reqMemo = useTestMemo!<string, IsomorphicRequest>();
163+
164+
mswServer?.use(
165+
rest.post("http://localhost/default/q/fingerprint", (req, res, ctx) => {
166+
reqMemo.set(testCtx.meta.id, req);
167+
return res(ctx.status(200), ctx.json(minSuccessfulJSON));
168+
}),
169+
rest.post("http://localhost/transformed", (req, res, ctx) => {
170+
reqMemo.set(testCtx.meta.id, req);
171+
return res(ctx.status(200), ctx.json(minSuccessfulJSON));
172+
})
173+
);
174+
});
175+
176+
it("transforms request headers", async ({ useTestMemo, meta }) => {
177+
const client = makeStubClient({
178+
strategies: {
179+
makeQueryURL: () =>
180+
Promise.resolve("http://localhost/default/q/fingerprint"),
181+
makeFetchOptions: () => {
182+
return {
183+
method: "POST",
184+
headers: {
185+
"initial-header": "stays",
186+
"override-header": "will-not-be-set-to-this",
187+
},
188+
};
189+
},
190+
transformFetchOptions({ input, init }) {
191+
return {
192+
input,
193+
init: {
194+
...init,
195+
headers: {
196+
...init?.headers,
197+
"new-header": "was-not-in-make-fetch-options",
198+
"override-header": "is-different-from-fetch-options",
199+
},
200+
},
201+
};
202+
},
203+
},
204+
});
205+
206+
const { error } = await client.execute<{}>("SELECT 1;");
207+
208+
const reqMemo = useTestMemo!().get(meta.id) as IsomorphicRequest;
209+
210+
expect(error).toBeNull();
211+
212+
expect(reqMemo["headers"]).toMatchInlineSnapshot(`
213+
HeadersPolyfill {
214+
Symbol(normalizedHeaders): {
215+
"initial-header": "stays",
216+
"new-header": "was-not-in-make-fetch-options",
217+
"override-header": "is-different-from-fetch-options",
218+
},
219+
Symbol(rawHeaderNames): Map {
220+
"initial-header" => "initial-header",
221+
"new-header" => "new-header",
222+
"override-header" => "override-header",
223+
},
224+
}
225+
`);
226+
});
227+
228+
it("transforms request URL", async ({ useTestMemo, meta }) => {
229+
const client = makeStubClient({
230+
strategies: {
231+
makeQueryURL: () =>
232+
Promise.resolve("http://localhost/default/q/fingerprint"),
233+
transformFetchOptions({ init }) {
234+
return {
235+
input: "http://localhost/transformed",
236+
init,
237+
};
238+
},
239+
},
240+
});
241+
242+
const { error } = await client.execute<{}>("SELECT 1;");
243+
244+
const reqMemo = useTestMemo!().get(meta.id) as IsomorphicRequest;
245+
246+
expect(error).toBeNull();
247+
248+
expect(reqMemo["url"].toString()).toBe("http://localhost/transformed");
249+
});
250+
});
251+
144252
describe("client handles errors correctly because it", () => {
145253
setupMswServerTestHooks();
146254

@@ -217,6 +325,7 @@ const makeUnconnectableClient = () => {
217325
},
218326
parseFieldsFromResponse: skipParsingFieldsFromResponse,
219327
parseFieldsFromResponseBodyJSON: parseFieldsFromResponseBodyJSONFieldsKey,
328+
transformFetchOptions: skipTransformFetchOptions,
220329
},
221330
});
222331
};

packages/client-http/client-http.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export class SqlHTTPClient<
140140
parseFieldsFromResponse: opts.strategies?.parseFieldsFromResponse,
141141
parseFieldsFromResponseBodyJSON:
142142
opts.strategies?.parseFieldsFromResponseBodyJSON,
143+
transformFetchOptions: opts.strategies?.transformFetchOptions,
143144
};
144145
}
145146

@@ -203,7 +204,16 @@ export class SqlHTTPClient<
203204
observedFields: {},
204205
};
205206

206-
const { response, error } = await fetch(queryURL, fetchOptions)
207+
const { input: transformedQueryURL, init: transformedFetchOptions } =
208+
this.strategies.transformFetchOptions({
209+
input: queryURL,
210+
init: fetchOptions,
211+
});
212+
213+
const { response, error } = await fetch(
214+
transformedQueryURL,
215+
transformedFetchOptions
216+
)
207217
.catch((err) => Promise.reject({ type: "network", ...err }))
208218
.then(async (r) => {
209219
if (r.type === "error") {

packages/client-http/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export type * from "./strategies/types";
44

55
export * from "./strategies/parseFieldsFromResponse";
66
export * from "./strategies/parseFieldsFromResponseBodyJSON";
7+
export * from "./strategies/transformFetchOptions";
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import type { HTTPStrategies } from "./types";
2+
3+
export const skipTransformFetchOptions: HTTPStrategies["transformFetchOptions"] =
4+
(opts) => opts;

packages/client-http/strategies/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ type MakeFetchOptionsStrategy = ({
5050
execOptions,
5151
}: MakeFetchOptionsStrategyArgs) => RequestInit;
5252

53+
type TransformFetchOptionsStrategy = ({
54+
input,
55+
init,
56+
}: {
57+
input: Parameters<typeof fetch>[0];
58+
init: Parameters<typeof fetch>[1];
59+
}) => {
60+
input: Parameters<typeof fetch>[0];
61+
init: Parameters<typeof fetch>[1];
62+
};
63+
5364
export type MakeQueryURLStrategyArgs = {
5465
database: Database;
5566
host: Host;
@@ -80,4 +91,5 @@ export type HTTPStrategies = {
8091
makeQueryURL: MakeQueryURLStrategy;
8192
parseFieldsFromResponse: ParseFieldsFromResponseStrategy;
8293
parseFieldsFromResponseBodyJSON: ParseFieldsFromResponseBodyJSONStrategy;
94+
transformFetchOptions: TransformFetchOptionsStrategy;
8395
};

packages/core/seafowl.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { Host } from "@madatdata/base-client";
66
import {
77
parseFieldsFromResponseContentTypeHeader,
88
skipParsingFieldsFromResponseBodyJSON,
9+
skipTransformFetchOptions,
910
} from "@madatdata/client-http";
1011
import { rest } from "msw";
1112

@@ -128,6 +129,7 @@ describe("arrow", () => {
128129
},
129130
parseFieldsFromResponse: parseFieldsFromResponseContentTypeHeader,
130131
parseFieldsFromResponseBodyJSON: skipParsingFieldsFromResponseBodyJSON,
132+
transformFetchOptions: skipTransformFetchOptions,
131133
},
132134
});
133135

@@ -405,6 +407,7 @@ describe("fields from header", () => {
405407
},
406408
parseFieldsFromResponse: parseFieldsFromResponseContentTypeHeader,
407409
parseFieldsFromResponseBodyJSON: skipParsingFieldsFromResponseBodyJSON,
410+
transformFetchOptions: skipTransformFetchOptions,
408411
},
409412
});
410413

@@ -460,6 +463,7 @@ describe("field inferrence", () => {
460463
},
461464
parseFieldsFromResponse: parseFieldsFromResponseContentTypeHeader,
462465
parseFieldsFromResponseBodyJSON: skipParsingFieldsFromResponseBodyJSON,
466+
transformFetchOptions: skipTransformFetchOptions,
463467
},
464468
});
465469

@@ -585,6 +589,7 @@ describe("makeSeafowlHTTPContext", () => {
585589
"makeQueryURL": [Function],
586590
"parseFieldsFromResponse": [Function],
587591
"parseFieldsFromResponseBodyJSON": [Function],
592+
"transformFetchOptions": [Function],
588593
},
589594
},
590595
"db": {

packages/core/splitgraph.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ describe("makeSplitgraphHTTPContext", () => {
7070
"makeQueryURL": [Function],
7171
"parseFieldsFromResponse": [Function],
7272
"parseFieldsFromResponseBodyJSON": [Function],
73+
"transformFetchOptions": [Function],
7374
},
7475
},
7576
"db": DbSplitgraph {

packages/db-seafowl/db-seafowl.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
makeClient as makeHTTPClient,
2626
parseFieldsFromResponseContentTypeHeader,
2727
skipParsingFieldsFromResponseBodyJSON,
28+
skipTransformFetchOptions,
2829
} from "@madatdata/client-http";
2930

3031
// FIXME: we _should_ only be depending on types from this pacakge - should
@@ -177,6 +178,7 @@ export class DbSeafowl<SeafowlPluginList extends PluginList>
177178
},
178179
parseFieldsFromResponse: parseFieldsFromResponseContentTypeHeader,
179180
parseFieldsFromResponseBodyJSON: skipParsingFieldsFromResponseBodyJSON,
181+
transformFetchOptions: skipTransformFetchOptions,
180182
},
181183
};
182184
}

packages/db-splitgraph/db-splitgraph.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import type { HTTPStrategies, HTTPClientOptions } from "@madatdata/client-http";
2727
import {
2828
parseFieldsFromResponseBodyJSONFieldsKey,
2929
skipParsingFieldsFromResponse,
30+
skipTransformFetchOptions,
3031
} from "@madatdata/client-http";
3132
import type { GraphQLClientOptions } from "./plugins";
3233

@@ -163,6 +164,7 @@ export class DbSplitgraph<SplitgraphPluginList extends PluginList>
163164
parseFieldsFromResponse: skipParsingFieldsFromResponse,
164165
parseFieldsFromResponseBodyJSON:
165166
parseFieldsFromResponseBodyJSONFieldsKey,
167+
transformFetchOptions: skipTransformFetchOptions,
166168
},
167169
}
168170
);

packages/react/hooks.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ describe("makeDefaultAnonymousContext", () => {
163163
"makeQueryURL": [Function],
164164
"parseFieldsFromResponse": [Function],
165165
"parseFieldsFromResponseBodyJSON": [Function],
166+
"transformFetchOptions": [Function],
166167
},
167168
},
168169
"db": DbSplitgraph {

0 commit comments

Comments
 (0)