Skip to content

Commit

Permalink
fix: custom endpoints with method: 'put' (#9037)
Browse files Browse the repository at this point in the history
### 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 #8807

-->
  • Loading branch information
r1tsuu authored Nov 5, 2024
1 parent f52b7c4 commit 9ce2ba6
Show file tree
Hide file tree
Showing 13 changed files with 218 additions and 9 deletions.
1 change: 1 addition & 0 deletions packages/next/src/exports/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
84 changes: 84 additions & 0 deletions packages/next/src/routes/rest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
}
}
10 changes: 9 additions & 1 deletion templates/_template/src/app/(payload)/api/[...slug]/route.ts
Original file line number Diff line number Diff line change
@@ -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)
10 changes: 9 additions & 1 deletion templates/blank/src/app/(payload)/api/[...slug]/route.ts
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
11 changes: 10 additions & 1 deletion templates/website/src/app/(payload)/api/[...slug]/route.ts
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
20 changes: 19 additions & 1 deletion test/collections-rest/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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,
Expand All @@ -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: {
Expand Down Expand Up @@ -257,6 +261,15 @@ export default buildConfigWithDefaults({
],
},
},
{
slug: endpointsSlug,
fields: [],
endpoints: methods.map((method) => ({
method,
handler: () => new Response(`${method} response`),
path: `/${method}-test`,
})),
},
],
endpoints: [
{
Expand Down Expand Up @@ -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({
Expand Down
21 changes: 21 additions & 0 deletions test/collections-rest/int.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { initPayloadInt } from '../helpers/initPayloadInt.js'
import {
customIdNumberSlug,
customIdSlug,
endpointsSlug,
errorOnHookSlug,
methods,
pointSlug,
relationSlug,
slug,
Expand Down Expand Up @@ -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>) {
Expand Down
20 changes: 20 additions & 0 deletions test/helpers/NextRESTClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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
Expand All @@ -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)
}

Expand Down Expand Up @@ -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 }) })
}
}

0 comments on commit 9ce2ba6

Please sign in to comment.