From 9ce2ba6a3f3988152a0a841ac38e1e2dd5195d45 Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Tue, 5 Nov 2024 23:14:34 +0200 Subject: [PATCH] fix: custom endpoints with `method: 'put'` (#9037) ### What? Fixes support for custom endpoints with `method: 'put'`. Previously, this didn't work: ```ts export default buildConfigWithDefaults({ collections: [ ], endpoints: [ { method: 'put', handler: () => new Response(), path: '/put', }, ], }) ``` ### Why? We supported this in 2.0 and docs are saying that we can use `'put'` as `method` https://payloadcms.com/docs/beta/rest-api/overview#custom-endpoints ### How? Implements the `REST_PUT` export for `@payloadcms/next/routes`, updates all templates. Additionally, adds tests to ensure root/collection level custom endpoints with all necessary methods execute properly. Fixes https://github.com/payloadcms/payload/issues/8807 --> --- packages/next/src/exports/routes.ts | 1 + packages/next/src/routes/rest/index.ts | 84 +++++++++++++++++++ .../src/app/(payload)/api/[...slug]/route.ts | 10 ++- .../src/app/(payload)/api/[...slug]/route.ts | 10 ++- .../src/app/(payload)/api/[...slug]/route.ts | 10 ++- .../src/app/(payload)/api/[...slug]/route.ts | 11 ++- .../src/app/(payload)/api/[...slug]/route.ts | 10 ++- .../src/app/(payload)/api/[...slug]/route.ts | 10 ++- .../src/app/(payload)/api/[...slug]/route.ts | 10 ++- .../src/app/(payload)/api/[...slug]/route.ts | 10 ++- test/collections-rest/config.ts | 20 ++++- test/collections-rest/int.spec.ts | 21 +++++ test/helpers/NextRESTClient.ts | 20 +++++ 13 files changed, 218 insertions(+), 9 deletions(-) diff --git a/packages/next/src/exports/routes.ts b/packages/next/src/exports/routes.ts index ef8617556..121d93579 100644 --- a/packages/next/src/exports/routes.ts +++ b/packages/next/src/exports/routes.ts @@ -6,4 +6,5 @@ export { OPTIONS as REST_OPTIONS, PATCH as REST_PATCH, POST as REST_POST, + PUT as REST_PUT, } from '../routes/rest/index.js' diff --git a/packages/next/src/routes/rest/index.ts b/packages/next/src/routes/rest/index.ts index 1c5dc3d21..58a707385 100644 --- a/packages/next/src/routes/rest/index.ts +++ b/packages/next/src/routes/rest/index.ts @@ -821,3 +821,87 @@ export const PATCH = }) } } + +export const PUT = + (config: Promise | SanitizedConfig) => + async (request: Request, { params: paramsPromise }: { params: Promise<{ slug: string[] }> }) => { + const { slug } = await paramsPromise + const [slug1] = slug + let req: PayloadRequest + let res: Response + let collection: Collection + + try { + req = await createPayloadRequest({ + config, + request, + }) + collection = req.payload.collections?.[slug1] + + const disableEndpoints = endpointsAreDisabled({ + endpoints: req.payload.config.endpoints, + request, + }) + if (disableEndpoints) { + return disableEndpoints + } + + if (collection) { + req.routeParams.collection = slug1 + + const disableEndpoints = endpointsAreDisabled({ + endpoints: collection.config.endpoints, + request, + }) + if (disableEndpoints) { + return disableEndpoints + } + + const customEndpointResponse = await handleCustomEndpoints({ + endpoints: collection.config.endpoints, + entitySlug: slug1, + req, + }) + + if (customEndpointResponse) { + return customEndpointResponse + } + } + + if (res instanceof Response) { + if (req.responseHeaders) { + const mergedResponse = new Response(res.body, { + headers: mergeHeaders(req.responseHeaders, res.headers), + status: res.status, + statusText: res.statusText, + }) + + return mergedResponse + } + + return res + } + + // root routes + const customEndpointResponse = await handleCustomEndpoints({ + endpoints: req.payload.config.endpoints, + req, + }) + + if (customEndpointResponse) { + return customEndpointResponse + } + + return RouteNotFoundResponse({ + slug, + req, + }) + } catch (error) { + return routeError({ + collection, + config, + err: error, + req: req || request, + }) + } + } diff --git a/templates/_template/src/app/(payload)/api/[...slug]/route.ts b/templates/_template/src/app/(payload)/api/[...slug]/route.ts index 183cf457f..52c556b03 100644 --- a/templates/_template/src/app/(payload)/api/[...slug]/route.ts +++ b/templates/_template/src/app/(payload)/api/[...slug]/route.ts @@ -1,10 +1,18 @@ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ import config from '@payload-config' -import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes' +import { + REST_DELETE, + REST_GET, + REST_OPTIONS, + REST_PATCH, + REST_POST, + REST_PUT, +} from '@payloadcms/next/routes' export const GET = REST_GET(config) export const POST = REST_POST(config) export const DELETE = REST_DELETE(config) export const PATCH = REST_PATCH(config) +export const PUT = REST_PUT(config) export const OPTIONS = REST_OPTIONS(config) diff --git a/templates/blank/src/app/(payload)/api/[...slug]/route.ts b/templates/blank/src/app/(payload)/api/[...slug]/route.ts index 183cf457f..52c556b03 100644 --- a/templates/blank/src/app/(payload)/api/[...slug]/route.ts +++ b/templates/blank/src/app/(payload)/api/[...slug]/route.ts @@ -1,10 +1,18 @@ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ import config from '@payload-config' -import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes' +import { + REST_DELETE, + REST_GET, + REST_OPTIONS, + REST_PATCH, + REST_POST, + REST_PUT, +} from '@payloadcms/next/routes' export const GET = REST_GET(config) export const POST = REST_POST(config) export const DELETE = REST_DELETE(config) export const PATCH = REST_PATCH(config) +export const PUT = REST_PUT(config) export const OPTIONS = REST_OPTIONS(config) diff --git a/templates/vercel-postgres/src/app/(payload)/api/[...slug]/route.ts b/templates/vercel-postgres/src/app/(payload)/api/[...slug]/route.ts index 183cf457f..52c556b03 100644 --- a/templates/vercel-postgres/src/app/(payload)/api/[...slug]/route.ts +++ b/templates/vercel-postgres/src/app/(payload)/api/[...slug]/route.ts @@ -1,10 +1,18 @@ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ import config from '@payload-config' -import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes' +import { + REST_DELETE, + REST_GET, + REST_OPTIONS, + REST_PATCH, + REST_POST, + REST_PUT, +} from '@payloadcms/next/routes' export const GET = REST_GET(config) export const POST = REST_POST(config) export const DELETE = REST_DELETE(config) export const PATCH = REST_PATCH(config) +export const PUT = REST_PUT(config) export const OPTIONS = REST_OPTIONS(config) diff --git a/templates/website/src/app/(payload)/api/[...slug]/route.ts b/templates/website/src/app/(payload)/api/[...slug]/route.ts index 183cf457f..cad010a25 100644 --- a/templates/website/src/app/(payload)/api/[...slug]/route.ts +++ b/templates/website/src/app/(payload)/api/[...slug]/route.ts @@ -1,10 +1,19 @@ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ import config from '@payload-config' -import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes' +import { + REST_DELETE, + REST_GET, + REST_OPTIONS, + REST_PATCH, + REST_POST, + REST_PUT, +} from '@payloadcms/next/routes' export const GET = REST_GET(config) export const POST = REST_POST(config) export const DELETE = REST_DELETE(config) export const PATCH = REST_PATCH(config) + +export const PUT = REST_PUT(config) export const OPTIONS = REST_OPTIONS(config) diff --git a/templates/with-payload-cloud/src/app/(payload)/api/[...slug]/route.ts b/templates/with-payload-cloud/src/app/(payload)/api/[...slug]/route.ts index 183cf457f..52c556b03 100644 --- a/templates/with-payload-cloud/src/app/(payload)/api/[...slug]/route.ts +++ b/templates/with-payload-cloud/src/app/(payload)/api/[...slug]/route.ts @@ -1,10 +1,18 @@ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ import config from '@payload-config' -import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes' +import { + REST_DELETE, + REST_GET, + REST_OPTIONS, + REST_PATCH, + REST_POST, + REST_PUT, +} from '@payloadcms/next/routes' export const GET = REST_GET(config) export const POST = REST_POST(config) export const DELETE = REST_DELETE(config) export const PATCH = REST_PATCH(config) +export const PUT = REST_PUT(config) export const OPTIONS = REST_OPTIONS(config) diff --git a/templates/with-postgres/src/app/(payload)/api/[...slug]/route.ts b/templates/with-postgres/src/app/(payload)/api/[...slug]/route.ts index 183cf457f..52c556b03 100644 --- a/templates/with-postgres/src/app/(payload)/api/[...slug]/route.ts +++ b/templates/with-postgres/src/app/(payload)/api/[...slug]/route.ts @@ -1,10 +1,18 @@ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ import config from '@payload-config' -import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes' +import { + REST_DELETE, + REST_GET, + REST_OPTIONS, + REST_PATCH, + REST_POST, + REST_PUT, +} from '@payloadcms/next/routes' export const GET = REST_GET(config) export const POST = REST_POST(config) export const DELETE = REST_DELETE(config) export const PATCH = REST_PATCH(config) +export const PUT = REST_PUT(config) export const OPTIONS = REST_OPTIONS(config) diff --git a/templates/with-vercel-mongodb/src/app/(payload)/api/[...slug]/route.ts b/templates/with-vercel-mongodb/src/app/(payload)/api/[...slug]/route.ts index 183cf457f..52c556b03 100644 --- a/templates/with-vercel-mongodb/src/app/(payload)/api/[...slug]/route.ts +++ b/templates/with-vercel-mongodb/src/app/(payload)/api/[...slug]/route.ts @@ -1,10 +1,18 @@ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ import config from '@payload-config' -import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes' +import { + REST_DELETE, + REST_GET, + REST_OPTIONS, + REST_PATCH, + REST_POST, + REST_PUT, +} from '@payloadcms/next/routes' export const GET = REST_GET(config) export const POST = REST_POST(config) export const DELETE = REST_DELETE(config) export const PATCH = REST_PATCH(config) +export const PUT = REST_PUT(config) export const OPTIONS = REST_OPTIONS(config) diff --git a/templates/with-vercel-postgres/src/app/(payload)/api/[...slug]/route.ts b/templates/with-vercel-postgres/src/app/(payload)/api/[...slug]/route.ts index 183cf457f..52c556b03 100644 --- a/templates/with-vercel-postgres/src/app/(payload)/api/[...slug]/route.ts +++ b/templates/with-vercel-postgres/src/app/(payload)/api/[...slug]/route.ts @@ -1,10 +1,18 @@ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ import config from '@payload-config' -import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes' +import { + REST_DELETE, + REST_GET, + REST_OPTIONS, + REST_PATCH, + REST_POST, + REST_PUT, +} from '@payloadcms/next/routes' export const GET = REST_GET(config) export const POST = REST_POST(config) export const DELETE = REST_DELETE(config) export const PATCH = REST_PATCH(config) +export const PUT = REST_PUT(config) export const OPTIONS = REST_OPTIONS(config) diff --git a/test/collections-rest/config.ts b/test/collections-rest/config.ts index d41148b3d..97c8a891f 100644 --- a/test/collections-rest/config.ts +++ b/test/collections-rest/config.ts @@ -2,7 +2,7 @@ import { fileURLToPath } from 'node:url' import path from 'path' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) -import { APIError, type CollectionConfig } from 'payload' +import { APIError, type CollectionConfig, type Endpoint } from 'payload' import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' import { devUser } from '../credentials.js' @@ -19,6 +19,8 @@ const openAccess = { update: () => true, } +export const methods: Endpoint['method'][] = ['get', 'delete', 'patch', 'post', 'put'] + const collectionWithName = (collectionSlug: string): CollectionConfig => { return { slug: collectionSlug, @@ -39,6 +41,8 @@ export const customIdSlug = 'custom-id' export const customIdNumberSlug = 'custom-id-number' export const errorOnHookSlug = 'error-on-hooks' +export const endpointsSlug = 'endpoints' + export default buildConfigWithDefaults({ admin: { importMap: { @@ -257,6 +261,15 @@ export default buildConfigWithDefaults({ ], }, }, + { + slug: endpointsSlug, + fields: [], + endpoints: methods.map((method) => ({ + method, + handler: () => new Response(`${method} response`), + path: `/${method}-test`, + })), + }, ], endpoints: [ { @@ -296,6 +309,11 @@ export default buildConfigWithDefaults({ method: 'get', path: '/api-error-here', }, + ...methods.map((method) => ({ + method, + handler: () => new Response(`${method} response`), + path: `/${method}-test`, + })), ], onInit: async (payload) => { await payload.create({ diff --git a/test/collections-rest/int.spec.ts b/test/collections-rest/int.spec.ts index 68ef995ef..b5392ee08 100644 --- a/test/collections-rest/int.spec.ts +++ b/test/collections-rest/int.spec.ts @@ -13,7 +13,9 @@ import { initPayloadInt } from '../helpers/initPayloadInt.js' import { customIdNumberSlug, customIdSlug, + endpointsSlug, errorOnHookSlug, + methods, pointSlug, relationSlug, slug, @@ -1646,6 +1648,25 @@ describe('collections-rest', () => { ).resolves.toBeNull() }) }) + + describe('Custom endpoints', () => { + it('should execute custom root endpoints', async () => { + for (const method of methods) { + const response = await restClient[method.toUpperCase()](`/${method}-test`, {}) + await expect(response.text()).resolves.toBe(`${method} response`) + } + }) + + it('should execute custom collection endpoints', async () => { + for (const method of methods) { + const response = await restClient[method.toUpperCase()]( + `/${endpointsSlug}/${method}-test`, + {}, + ) + await expect(response.text()).resolves.toBe(`${method} response`) + } + }) + }) }) async function createPost(overrides?: Partial) { diff --git a/test/helpers/NextRESTClient.ts b/test/helpers/NextRESTClient.ts index efc1293ae..fbc89674b 100644 --- a/test/helpers/NextRESTClient.ts +++ b/test/helpers/NextRESTClient.ts @@ -7,6 +7,7 @@ import { GRAPHQL_POST as createGraphqlPOST, REST_PATCH as createPATCH, REST_POST as createPOST, + REST_PUT as createPUT, } from '@payloadcms/next/routes' import * as qs from 'qs-esm' @@ -67,6 +68,11 @@ export class NextRESTClient { args: { params: Promise<{ slug: string[] }> }, ) => Promise + private _PUT: ( + request: Request, + args: { params: Promise<{ slug: string[] }> }, + ) => Promise + private readonly config: SanitizedConfig private token: string @@ -82,6 +88,7 @@ export class NextRESTClient { this._POST = createPOST(config) this._DELETE = createDELETE(config) this._PATCH = createPATCH(config) + this._PUT = createPUT(config) this._GRAPHQL_POST = createGraphqlPOST(config) } @@ -221,4 +228,17 @@ export class NextRESTClient { }) return this._POST(request, { params: Promise.resolve({ slug }) }) } + + async PUT(path: ValidPath, options: FileArg & RequestInit & RequestOptions): Promise { + const { slug, params, url } = this.generateRequestParts(path) + const { query, ...rest } = options + const queryParams = generateQueryString(query, params) + + const request = new Request(`${url}${queryParams}`, { + ...rest, + headers: this.buildHeaders(options), + method: 'PUT', + }) + return this._PUT(request, { params: Promise.resolve({ slug }) }) + } }