diff --git a/packages/next-drupal/package.json b/packages/next-drupal/package.json index e308ad53..9adeaf05 100644 --- a/packages/next-drupal/package.json +++ b/packages/next-drupal/package.json @@ -45,7 +45,8 @@ }, "license": "MIT", "publishConfig": { - "access": "public" + "access": "public", + "registry": "https://registry.npmjs.org" }, "author": { "name": "shadcn", diff --git a/packages/next-drupal/src/next-drupal.ts b/packages/next-drupal/src/next-drupal.ts index 564e25eb..b4684e93 100644 --- a/packages/next-drupal/src/next-drupal.ts +++ b/packages/next-drupal/src/next-drupal.ts @@ -19,6 +19,7 @@ import type { JsonApiUpdateResourceBody, JsonApiWithAuthOption, JsonApiWithCacheOptions, + JsonApiWithNextFetchOptions, JsonDeserializer, Locale, NextDrupalOptions, @@ -248,7 +249,9 @@ export class NextDrupal extends NextDrupalBase { async getResource( type: string, uuid: string, - options?: JsonApiOptions & JsonApiWithCacheOptions + options?: JsonApiOptions & + JsonApiWithCacheOptions & + JsonApiWithNextFetchOptions ): Promise { options = { deserialize: true, @@ -257,6 +260,7 @@ export class NextDrupal extends NextDrupalBase { params: {}, ...options, } + // Not sure about this node-cache /* c8 ignore next 11 */ if (options.withCache) { @@ -283,6 +287,7 @@ export class NextDrupal extends NextDrupalBase { const response = await this.fetch(endpoint, { withAuth: options.withAuth, + next: options.next, }) await this.throwIfJsonErrors(response, "Error while fetching resource: ") @@ -301,7 +306,8 @@ export class NextDrupal extends NextDrupalBase { path: string, options?: { isVersionable?: boolean - } & JsonApiOptions + } & JsonApiOptions & + JsonApiWithNextFetchOptions ): Promise { options = { deserialize: true, @@ -370,6 +376,7 @@ export class NextDrupal extends NextDrupalBase { redirect: "follow", body: JSON.stringify(payload), withAuth: options.withAuth, + next: options.next, }) const errorMessagePrefix = "Error while fetching resource by path:" @@ -408,7 +415,8 @@ export class NextDrupal extends NextDrupalBase { type: string, options?: { deserialize?: boolean - } & JsonApiOptions + } & JsonApiOptions & + JsonApiWithNextFetchOptions ): Promise { options = { withAuth: this.withAuth, @@ -427,6 +435,7 @@ export class NextDrupal extends NextDrupalBase { const response = await this.fetch(endpoint, { withAuth: options.withAuth, + next: options.next, }) await this.throwIfJsonErrors( @@ -445,6 +454,7 @@ export class NextDrupal extends NextDrupalBase { pathPrefix?: PathPrefix params?: JsonApiParams } & JsonApiWithAuthOption & + JsonApiWithNextFetchOptions & ( | { locales: Locale[] @@ -490,6 +500,7 @@ export class NextDrupal extends NextDrupalBase { let opts: Parameters[1] = { params, withAuth: options.withAuth, + next: options.next, } if (locale) { opts = { @@ -547,7 +558,7 @@ export class NextDrupal extends NextDrupalBase { async translatePath( path: string, - options?: JsonApiWithAuthOption + options?: JsonApiWithAuthOption & JsonApiWithNextFetchOptions ): Promise { options = { withAuth: this.withAuth, @@ -562,6 +573,7 @@ export class NextDrupal extends NextDrupalBase { const response = await this.fetch(endpoint, { withAuth: options.withAuth, + next: options.next, }) if (response.status === 404) { @@ -575,7 +587,10 @@ export class NextDrupal extends NextDrupalBase { return await response.json() } - async getIndex(locale?: Locale): Promise { + async getIndex( + locale?: Locale, + options?: JsonApiWithNextFetchOptions + ): Promise { const endpoint = await this.buildEndpoint({ locale, }) @@ -585,6 +600,7 @@ export class NextDrupal extends NextDrupalBase { const response = await this.fetch(endpoint, { // As per https://www.drupal.org/node/2984034 /jsonapi is public. withAuth: false, + next: options?.next, }) await this.throwIfJsonErrors( @@ -657,7 +673,9 @@ export class NextDrupal extends NextDrupalBase { async getMenu( menuName: string, - options?: JsonApiOptions & JsonApiWithCacheOptions + options?: JsonApiOptions & + JsonApiWithCacheOptions & + JsonApiWithNextFetchOptions ): Promise<{ items: T[] tree: T[] @@ -692,6 +710,7 @@ export class NextDrupal extends NextDrupalBase { const response = await this.fetch(endpoint, { withAuth: options.withAuth, + next: options.next, }) await this.throwIfJsonErrors(response, "Error while fetching menu items: ") @@ -719,7 +738,7 @@ export class NextDrupal extends NextDrupalBase { async getView( name: string, - options?: JsonApiOptions + options?: JsonApiOptions & JsonApiWithNextFetchOptions ): Promise> { options = { withAuth: this.withAuth, @@ -741,6 +760,7 @@ export class NextDrupal extends NextDrupalBase { const response = await this.fetch(endpoint, { withAuth: options.withAuth, + next: options.next, }) await this.throwIfJsonErrors(response, "Error while fetching view: ") @@ -759,7 +779,7 @@ export class NextDrupal extends NextDrupalBase { async getSearchIndex( name: string, - options?: JsonApiOptions + options?: JsonApiOptions & JsonApiWithNextFetchOptions ): Promise { options = { withAuth: this.withAuth, @@ -778,6 +798,7 @@ export class NextDrupal extends NextDrupalBase { const response = await this.fetch(endpoint, { withAuth: options.withAuth, + next: options.next, }) await this.throwIfJsonErrors( diff --git a/packages/next-drupal/src/types/options.ts b/packages/next-drupal/src/types/options.ts index 14c8750c..ae18d2b4 100644 --- a/packages/next-drupal/src/types/options.ts +++ b/packages/next-drupal/src/types/options.ts @@ -6,7 +6,7 @@ export type Locale = string export type PathPrefix = string -export interface FetchOptions extends RequestInit { +export interface FetchOptions extends RequestInit, JsonApiWithNextFetchOptions { withAuth?: boolean | NextDrupalAuth } @@ -34,6 +34,9 @@ export type JsonApiWithCacheOptions = { cacheKey?: string } +export type JsonApiWithNextFetchOptions = { + next?: NextFetchRequestConfig +} // TODO: Properly type this. /* eslint-disable @typescript-eslint/no-explicit-any */ export type JsonApiParams = Record diff --git a/packages/next-drupal/tests/NextDrupal/resource-methods.test.ts b/packages/next-drupal/tests/NextDrupal/resource-methods.test.ts index 8e7e23fa..35389f2c 100644 --- a/packages/next-drupal/tests/NextDrupal/resource-methods.test.ts +++ b/packages/next-drupal/tests/NextDrupal/resource-methods.test.ts @@ -7,7 +7,11 @@ import { spyOnDrupalFetch, spyOnFetch, } from "../utils" -import type { DrupalNode, DrupalSearchApiJsonApiResponse } from "../../src" +import type { + DrupalNode, + DrupalSearchApiJsonApiResponse, + FetchOptions, +} from "../../src" jest.setTimeout(10000) @@ -158,6 +162,24 @@ describe("getIndex()", () => { "Failed to fetch JSON:API index at https://example.com/jsonapi" ) }) + + test("makes request with Next revalidate option", async () => { + const drupal = new NextDrupal(BASE_URL) + const mockInit = { + next: { revalidate: 60 }, + } as FetchOptions + + const fetchSpy = spyOnFetch() + + await drupal.getIndex(undefined, mockInit) + + expect(fetchSpy).toBeCalledWith( + expect.anything(), + expect.objectContaining({ + ...mockInit, + }) + ) + }) }) describe("getMenu()", () => { @@ -223,6 +245,42 @@ describe("getMenu()", () => { (fetchSpy.mock.lastCall[1].headers as Headers).get("Authorization") ).toBe("Bearer sample-token") }) + + test("makes request with Next revalidate option", async () => { + const drupal = new NextDrupal(BASE_URL) + const mockInit = { + next: { revalidate: 60 }, + } as FetchOptions + + const fetchSpy = spyOnFetch() + + await drupal.getMenu("main", mockInit) + + expect(fetchSpy).toBeCalledWith( + expect.anything(), + expect.objectContaining({ + ...mockInit, + }) + ) + }) + + test("makes request with Next tags option", async () => { + const drupal = new NextDrupal(BASE_URL) + const mockInit = { + next: { tags: ["tagged-resource"] }, + } as FetchOptions + + const fetchSpy = spyOnFetch() + + await drupal.getMenu("main", mockInit) + + expect(fetchSpy).toBeCalledWith( + expect.anything(), + expect.objectContaining({ + ...mockInit, + }) + ) + }) }) describe("getResource()", () => { @@ -408,6 +466,52 @@ describe("getResource()", () => { (fetchSpy.mock.lastCall[1].headers as Headers).get("Authorization") ).toBe("Bearer sample-token") }) + + test("makes request with Next revalidate option", async () => { + const drupal = new NextDrupal(BASE_URL) + const mockInit = { + next: { revalidate: 60 }, + } as FetchOptions + + const fetchSpy = spyOnFetch() + + await drupal.getResource( + "node--recipe", + "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + mockInit + ) + + expect(fetchSpy).toBeCalledTimes(1) + expect(fetchSpy).toBeCalledWith( + expect.anything(), + expect.objectContaining({ + ...mockInit, + }) + ) + }) + + test("makes request with Next tags option", async () => { + const drupal = new NextDrupal(BASE_URL) + const mockInit = { + next: { tags: ["tagged-resource"] }, + } as FetchOptions + + const fetchSpy = spyOnFetch() + + await drupal.getResource( + "node--recipe", + "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + mockInit + ) + + expect(fetchSpy).toBeCalledTimes(1) + expect(fetchSpy).toBeCalledWith( + expect.anything(), + expect.objectContaining({ + ...mockInit, + }) + ) + }) }) describe("getResourceByPath()", () => { @@ -676,6 +780,56 @@ describe("getResourceByPath()", () => { const resource = await drupal.getResourceByPath("") expect(resource).toBe(null) }) + + test("makes request with Next revalidate option", async () => { + const drupal = new NextDrupal(BASE_URL) + const mockInit = { + next: { revalidate: 60 }, + } as FetchOptions + + const fetchSpy = spyOnFetch({ + status: 207, + statusText: "Multi-Status", + responseBody: mocks.resources.subRequests.ok, + }) + + await drupal.getResourceByPath( + "/recipes/deep-mediterranean-quiche", + mockInit + ) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + ...mockInit, + }) + ) + }) + + test("makes request with Next tags option", async () => { + const drupal = new NextDrupal(BASE_URL) + const mockInit = { + next: { tags: ["tagged-resource"] }, + } as FetchOptions + + const fetchSpy = spyOnFetch({ + status: 207, + statusText: "Multi-Status", + responseBody: mocks.resources.subRequests.ok, + }) + + await drupal.getResourceByPath( + "/recipes/deep-mediterranean-quiche", + mockInit + ) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + ...mockInit, + }) + ) + }) }) describe("getResourceCollection()", () => { @@ -767,6 +921,42 @@ describe("getResourceCollection()", () => { (fetchSpy.mock.lastCall[1].headers as Headers).get("Authorization") ).toBe("Bearer sample-token") }) + + test("makes request with Next revalidate option", async () => { + const drupal = new NextDrupal(BASE_URL) + const mockInit = { + next: { revalidate: 60 }, + } as FetchOptions + + const fetchSpy = spyOnFetch() + + await drupal.getResourceCollection("node--recipe", mockInit) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + ...mockInit, + }) + ) + }) + + test("makes request with Next tags option", async () => { + const drupal = new NextDrupal(BASE_URL) + const mockInit = { + next: { tags: ["tagged-resource"] }, + } as FetchOptions + + const fetchSpy = spyOnFetch() + + await drupal.getResourceCollection("node--recipe", mockInit) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + ...mockInit, + }) + ) + }) }) describe("getResourceCollectionPathSegments()", () => { @@ -886,6 +1076,46 @@ describe("getResourceCollectionPathSegments()", () => { (fetchSpy.mock.lastCall[1].headers as Headers).get("Authorization") ).toBe(mockAuth) }) + + test("makes request with Next revalidate option", async () => { + const drupal = new NextDrupal(BASE_URL) + const mockInit = { + next: { revalidate: 60 }, + } as FetchOptions + + const fetchSpy = spyOnFetch({ + responseBody: { data: [] }, + }) + + await drupal.getResourceCollectionPathSegments("node--article", mockInit) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + ...mockInit, + }) + ) + }) + + test("makes request with Next tags option", async () => { + const drupal = new NextDrupal(BASE_URL) + const mockInit = { + next: { tags: ["tagged-resource"] }, + } as FetchOptions + + const fetchSpy = spyOnFetch({ + responseBody: { data: [] }, + }) + + await drupal.getResourceCollectionPathSegments("node--article", mockInit) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + ...mockInit, + }) + ) + }) }) describe("getSearchIndex()", () => { @@ -980,6 +1210,42 @@ describe("getSearchIndex()", () => { (fetchSpy.mock.lastCall[1].headers as Headers).get("Authorization") ).toBe("Bearer sample-token") }) + + test("makes request with Next revalidate option", async () => { + const drupal = new NextDrupal(BASE_URL) + const mockInit = { + next: { revalidate: 60 }, + } as FetchOptions + + const fetchSpy = spyOnFetch() + + await drupal.getSearchIndex("recipes", mockInit) + + expect(fetchSpy).toBeCalledWith( + expect.anything(), + expect.objectContaining({ + ...mockInit, + }) + ) + }) + + test("makes request with Next tags option", async () => { + const drupal = new NextDrupal(BASE_URL) + const mockInit = { + next: { tags: ["tagged-resource"] }, + } as FetchOptions + + const fetchSpy = spyOnFetch() + + await drupal.getSearchIndex("recipes", mockInit) + + expect(fetchSpy).toBeCalledWith( + expect.anything(), + expect.objectContaining({ + ...mockInit, + }) + ) + }) }) describe("getView()", () => { @@ -1067,6 +1333,42 @@ describe("getView()", () => { expect(view.links).toHaveProperty("next") }) + + test("makes request with Next revalidate option", async () => { + const drupal = new NextDrupal(BASE_URL) + const mockInit = { + next: { revalidate: 60 }, + } as FetchOptions + + const fetchSpy = spyOnFetch() + + await drupal.getView("featured_articles--page_1", mockInit) + + expect(fetchSpy).toBeCalledWith( + expect.anything(), + expect.objectContaining({ + ...mockInit, + }) + ) + }) + + test("makes request with Next tags option", async () => { + const drupal = new NextDrupal(BASE_URL) + const mockInit = { + next: { tags: ["tagged-resource"] }, + } as FetchOptions + + const fetchSpy = spyOnFetch() + + await drupal.getView("featured_articles--page_1", mockInit) + + expect(fetchSpy).toBeCalledWith( + expect.anything(), + expect.objectContaining({ + ...mockInit, + }) + ) + }) }) describe("translatePath()", () => { @@ -1140,4 +1442,40 @@ describe("translatePath()", () => { (fetchSpy.mock.lastCall[1].headers as Headers).get("Authorization") ).toBe("Bearer sample-token") }) + + test("makes request with Next revalidate option", async () => { + const drupal = new NextDrupal(BASE_URL) + const mockInit = { + next: { revalidate: 60 }, + } as FetchOptions + + const fetchSpy = spyOnFetch() + + await drupal.translatePath("recipes/deep-mediterranean-quiche", mockInit) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + ...mockInit, + }) + ) + }) + + test("makes request with Next tags option", async () => { + const drupal = new NextDrupal(BASE_URL) + const mockInit = { + next: { tags: ["tagged-resource"] }, + } as FetchOptions + + const fetchSpy = spyOnFetch() + + await drupal.translatePath("recipes/deep-mediterranean-quiche", mockInit) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + ...mockInit, + }) + ) + }) }) diff --git a/packages/next-drupal/tests/NextDrupalBase/fetch-methods.test.ts b/packages/next-drupal/tests/NextDrupalBase/fetch-methods.test.ts index 072b91fa..cc08554d 100644 --- a/packages/next-drupal/tests/NextDrupalBase/fetch-methods.test.ts +++ b/packages/next-drupal/tests/NextDrupalBase/fetch-methods.test.ts @@ -179,6 +179,27 @@ describe("fetch()", () => { authHeader ) }) + + test("optionally adds Next revalidate options", async () => { + const drupal = new NextDrupalBase(BASE_URL) + const mockUrl = "/mock-url" + const mockInit = { + next: { revalidate: 60 }, + } as FetchOptions + + const fetchSpy = spyOnFetch() + + await drupal.fetch(mockUrl, mockInit) + + expect(fetchSpy).toBeCalledTimes(1) + expect(fetchSpy).toBeCalledWith( + `${BASE_URL}${mockUrl}`, + expect.objectContaining({ + ...defaultInit, + ...mockInit, + }) + ) + }) }) describe("getAccessToken()", () => {