Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add fetchOptions parameter to control fetch() #291

Merged
merged 2 commits into from
May 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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