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
-->
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -821,3 +821,87 @@ export const PATCH =
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const PUT =
|
||||
(config: Promise<SanitizedConfig> | 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<Post>) {
|
||||
|
||||
@@ -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<Response>
|
||||
|
||||
private _PUT: (
|
||||
request: Request,
|
||||
args: { params: Promise<{ slug: string[] }> },
|
||||
) => Promise<Response>
|
||||
|
||||
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<Response> {
|
||||
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 }) })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user