Skip to content

Commit

Permalink
feat: add fetchOptions parameter to control fetch() (#291)
Browse files Browse the repository at this point in the history
* feat: add `fetchOptions` parameter

* test: `fetchOptions`
  • Loading branch information
angeloashmore authored May 15, 2023
1 parent 8f36b01 commit a492a40
Show file tree
Hide file tree
Showing 34 changed files with 375 additions and 54 deletions.
44 changes: 38 additions & 6 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
FetchLike,
HttpRequestLike,
ExtractDocumentType,
RequestInitLike,
} from "./types";
import { ForbiddenError } from "./ForbiddenError";
import { NotFoundError } from "./NotFoundError";
Expand Down Expand Up @@ -164,18 +165,33 @@ export type ClientConfig = {
* Node.js, this function must be provided.
*/
fetch?: FetchLike;

/**
* Options provided to the client's `fetch()` on all network requests. These
* options will be merged with internally required options. They can also be
* overriden on a per-query basis using the query's `fetchOptions` parameter.
*/
fetchOptions?: RequestInitLike;
};

/**
* Parameters for any client method that use `fetch()`. Only a subset of
* `fetch()` parameters are exposed.
* Parameters for any client method that use `fetch()`.
*/
type FetchParams = {
/**
* Options provided to the client's `fetch()` on all network requests. These
* options will be merged with internally required options. They can also be
* overriden on a per-query basis using the query's `fetchOptions` parameter.
*/
fetchOptions?: RequestInitLike;

/**
* An `AbortSignal` provided by an `AbortController`. This allows the network
* request to be cancelled if necessary.
*
* {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal}
*
* @deprecated Move the `signal` parameter into `fetchOptions.signal`.
*/
signal?: AbortSignalLike;
};
Expand Down Expand Up @@ -341,6 +357,8 @@ export class Client<
*/
fetchFn: FetchLike;

fetchOptions?: RequestInitLike;

/**
* Default parameters that will be sent with each query. These parameters can
* be overridden on each query if needed.
Expand Down Expand Up @@ -404,6 +422,7 @@ export class Client<
this.accessToken = options.accessToken;
this.routes = options.routes;
this.brokenRoute = options.brokenRoute;
this.fetchOptions = options.fetchOptions;
this.defaultParams = options.defaultParams;

if (options.ref) {
Expand Down Expand Up @@ -1269,12 +1288,15 @@ export class Client<
*/
async buildQueryURL({
signal,
fetchOptions,
...params
}: Partial<BuildQueryURLArgs> & FetchParams = {}): Promise<string> {
const ref = params.ref || (await this.getResolvedRefString());
const ref =
params.ref || (await this.getResolvedRefString({ signal, fetchOptions }));
const integrationFieldsRef =
params.integrationFieldsRef ||
(await this.getCachedRepository({ signal })).integrationFieldsRef ||
(await this.getCachedRepository({ signal, fetchOptions }))
.integrationFieldsRef ||
undefined;

return buildQueryURL(this.endpoint, {
Expand Down Expand Up @@ -1338,9 +1360,10 @@ export class Client<

if (documentID != null && previewToken != null) {
const document = await this.getByID(documentID, {
signal: args.signal,
ref: previewToken,
lang: "*",
signal: args.signal,
fetchOptions: args.fetchOptions,
});

const url = prismicH.asLink(document, args.linkResolver);
Expand Down Expand Up @@ -1688,7 +1711,16 @@ export class Client<
// : {};

const res = await this.fetchFn(url, {
signal: params.signal,
...this.fetchOptions,
...params.fetchOptions,
headers: {
...this.fetchOptions?.headers,
...params.fetchOptions?.headers,
},
signal:
params.fetchOptions?.signal ||
params.signal ||
this.fetchOptions?.signal,
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
17 changes: 12 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,23 @@ export type FetchLike = (
export type AbortSignalLike = any;

/**
* The minimum required properties from RequestInit.
* A subset of RequestInit properties to configure a `fetch()` request.
*/
export interface RequestInitLike {
// Only options relevant to the client are included. Extending from the full
// RequestInit would cause issues, such as accepting Header objects.
//
// An interface is used to allow other libraries to augment the type with
// environment-specific types.
export interface RequestInitLike extends Pick<RequestInit, "cache"> {
/**
* An object literal to set the `fetch()` request's headers.
*/
headers?: Record<string, string>;

/**
* An object that allows you to abort a `fetch()` request if needed via an
* `AbortController` object
* An AbortSignal to set the `fetch()` request's signal.
*
* {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal}
* See: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
*/
// NOTE: `AbortSignalLike` is `any`! It is left as `AbortSignalLike`
// for backwards compatibility (the type is exported) and to signal to
Expand Down
34 changes: 29 additions & 5 deletions test/__testutils__/testAbortableMethod.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { it, expect } from "vitest";
import { expect, it } from "vitest";

import { mockPrismicRestAPIV2 } from "./mockPrismicRestAPIV2";
import { createTestClient } from "./createClient";
import { mockPrismicRestAPIV2 } from "./mockPrismicRestAPIV2";

import * as prismic from "../../src";

type TestAbortableMethodArgs = {
run: (
client: prismic.Client,
signal: prismic.AbortSignalLike,
params?: Parameters<prismic.Client["get"]>[0],
) => Promise<unknown>;
};

export const testAbortableMethod = (
description: string,
args: TestAbortableMethodArgs,
) => {
): void => {
it.concurrent(description, async (ctx) => {
const controller = new AbortController();
controller.abort();
Expand All @@ -25,7 +25,31 @@ export const testAbortableMethod = (
const client = createTestClient();

await expect(async () => {
await args.run(client, controller.signal);
await args.run(client, {
fetchOptions: {
signal: controller.signal,
},
});
}).rejects.toThrow(/aborted/i);
});

// TODO: Remove once the `signal` parameter is removed in favor of
// `fetchOptions.signal`.
it.concurrent(
`${description} (using deprecated \`signal\` param)`,
async (ctx) => {
const controller = new AbortController();
controller.abort();

mockPrismicRestAPIV2({ ctx });

const client = createTestClient();

await expect(async () => {
await args.run(client, {
signal: controller.signal,
});
}).rejects.toThrow(/aborted/i);
},
);
};
98 changes: 98 additions & 0 deletions test/__testutils__/testFetchOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import * as prismic from "../../src";
import { createTestClient } from "./createClient";
import { mockPrismicRestAPIV2 } from "./mockPrismicRestAPIV2";
import fetch from "node-fetch";
import { expect, it, vi } from "vitest";

type TestFetchOptionsArgs = {
run: (
client: prismic.Client,
params?: Parameters<prismic.Client["get"]>[0],
) => Promise<unknown>;
};

export const testFetchOptions = (
description: string,
args: TestFetchOptionsArgs,
): void => {
it.concurrent(`${description} (on client)`, async (ctx) => {
const abortController = new AbortController();

const fetchSpy = vi.fn(fetch);
const fetchOptions: prismic.RequestInitLike = {
cache: "no-store",
headers: {
foo: "bar",
},
signal: abortController.signal,
};

const repositoryResponse = ctx.mock.api.repository();
const masterRef = ctx.mock.api.ref({ isMasterRef: true });
const releaseRef = ctx.mock.api.ref({ isMasterRef: false });
releaseRef.id = "id"; // Referenced in ref-related tests.
releaseRef.label = "label"; // Referenced in ref-related tests.
repositoryResponse.refs = [masterRef, releaseRef];

mockPrismicRestAPIV2({
ctx,
repositoryResponse,
queryResponse: ctx.mock.api.query({
documents: [ctx.mock.value.document()],
}),
});

const client = createTestClient({
clientConfig: {
fetch: fetchSpy,
fetchOptions,
},
});

await args.run(client);

for (const [input, init] of fetchSpy.mock.calls) {
expect(init, input.toString()).toStrictEqual(fetchOptions);
}
});

it.concurrent(`${description} (on method)`, async (ctx) => {
const abortController = new AbortController();

const fetchSpy = vi.fn(fetch);
const fetchOptions: prismic.RequestInitLike = {
cache: "no-store",
headers: {
foo: "bar",
},
signal: abortController.signal,
};

const repositoryResponse = ctx.mock.api.repository();
const masterRef = ctx.mock.api.ref({ isMasterRef: true });
const releaseRef = ctx.mock.api.ref({ isMasterRef: false });
releaseRef.id = "id"; // Referenced in ref-related tests.
releaseRef.label = "label"; // Referenced in ref-related tests.
repositoryResponse.refs = [masterRef, releaseRef];

mockPrismicRestAPIV2({
ctx,
repositoryResponse,
queryResponse: ctx.mock.api.query({
documents: [ctx.mock.value.document()],
}),
});

const client = createTestClient({
clientConfig: {
fetch: fetchSpy,
},
});

await args.run(client, { fetchOptions });

for (const [input, init] of fetchSpy.mock.calls) {
expect(init, input.toString()).toStrictEqual(fetchOptions);
}
});
};
7 changes: 6 additions & 1 deletion test/client-dangerouslyGetAll.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { testAbortableMethod } from "./__testutils__/testAbortableMethod";
import { testGetAllMethod } from "./__testutils__/testAnyGetMethod";

import { GET_ALL_QUERY_DELAY } from "../src/client";
import { testFetchOptions } from "./__testutils__/testFetchOptions";

/**
* Tolerance in number of milliseconds for the duration of a simulated network
Expand Down Expand Up @@ -178,6 +179,10 @@ it("does not throttle single page queries", async (ctx) => {
).toBe(true);
});

testFetchOptions("supports fetch options", {
run: (client, params) => client.dangerouslyGetAll(params),
});

testAbortableMethod("is abortable with an AbortController", {
run: (client, signal) => client.dangerouslyGetAll({ signal }),
run: (client, params) => client.dangerouslyGetAll(params),
});
7 changes: 6 additions & 1 deletion test/client-get.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { testGetMethod } from "./__testutils__/testAnyGetMethod";
import { testAbortableMethod } from "./__testutils__/testAbortableMethod";
import { testFetchOptions } from "./__testutils__/testFetchOptions";

testGetMethod("resolves a query", {
run: (client) => client.get(),
Expand Down Expand Up @@ -52,6 +53,10 @@ testGetMethod("merges params and default params if provided", {
},
});

testFetchOptions("supports fetch options", {
run: (client, params) => client.get(params),
});

testAbortableMethod("is abortable with an AbortController", {
run: (client, signal) => client.get({ signal }),
run: (client, params) => client.get(params),
});
7 changes: 6 additions & 1 deletion test/client-getAllByEveryTag.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { testGetAllMethod } from "./__testutils__/testAnyGetMethod";
import { testAbortableMethod } from "./__testutils__/testAbortableMethod";
import { testFetchOptions } from "./__testutils__/testFetchOptions";

testGetAllMethod("returns all documents by every tag from paginated response", {
run: (client) => client.getAllByEveryTag(["foo", "bar"]),
Expand All @@ -23,6 +24,10 @@ testGetAllMethod("includes params if provided", {
},
});

testFetchOptions("supports fetch options", {
run: (client, params) => client.getAllByEveryTag(["foo", "bar"], params),
});

testAbortableMethod("is abortable with an AbortController", {
run: (client, signal) => client.getAllByEveryTag(["foo", "bar"], { signal }),
run: (client, params) => client.getAllByEveryTag(["foo", "bar"], params),
});
7 changes: 6 additions & 1 deletion test/client-getAllByIDs.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { testGetAllMethod } from "./__testutils__/testAnyGetMethod";
import { testAbortableMethod } from "./__testutils__/testAbortableMethod";
import { testFetchOptions } from "./__testutils__/testFetchOptions";

testGetAllMethod("returns all documents by IDs from paginated response", {
run: (client) => client.getAllByIDs(["id1", "id2"]),
Expand All @@ -23,6 +24,10 @@ testGetAllMethod("includes params if provided", {
},
});

testFetchOptions("supports fetch options", {
run: (client, params) => client.getAllByIDs(["id1", "id2"], params),
});

testAbortableMethod("is abortable with an AbortController", {
run: (client, signal) => client.getAllByIDs(["id1", "id2"], { signal }),
run: (client, params) => client.getAllByIDs(["id1", "id2"], params),
});
7 changes: 6 additions & 1 deletion test/client-getAllBySomeTags.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { testGetAllMethod } from "./__testutils__/testAnyGetMethod";
import { testAbortableMethod } from "./__testutils__/testAbortableMethod";
import { testFetchOptions } from "./__testutils__/testFetchOptions";

testGetAllMethod("returns all documents by some tags from paginated response", {
run: (client) => client.getAllBySomeTags(["foo", "bar"]),
Expand All @@ -23,6 +24,10 @@ testGetAllMethod("includes params if provided", {
},
});

testFetchOptions("supports fetch options", {
run: (client, params) => client.getAllBySomeTags(["foo", "bar"], params),
});

testAbortableMethod("is abortable with an AbortController", {
run: (client, signal) => client.getAllBySomeTags(["foo", "bar"], { signal }),
run: (client, params) => client.getAllBySomeTags(["foo", "bar"], params),
});
Loading

0 comments on commit a492a40

Please sign in to comment.