payload
Version:
Node, React, Headless CMS and Application Framework built on Next.js
187 lines (186 loc) • 6.62 kB
JavaScript
// @ts-strict-ignore
import { status as httpStatus } from 'http-status';
import { match } from 'path-to-regexp';
import { createPayloadRequest } from './createPayloadRequest.js';
import { headersWithCors } from './headersWithCors.js';
import { mergeHeaders } from './mergeHeaders.js';
import { routeError } from './routeError.js';
const notFoundResponse = (req, pathname)=>{
return Response.json({
message: `Route not found "${pathname ?? new URL(req.url).pathname}"`
}, {
headers: headersWithCors({
headers: new Headers(),
req
}),
status: httpStatus.NOT_FOUND
});
};
/**
* Attaches the Payload REST API to any backend framework that uses Fetch Request/Response
* like Next.js (app router), Remix, Bun, Hono.
*
* ### Example: Using Hono
* ```ts
* import { handleEndpoints } from 'payload';
* import { serve } from '@hono/node-server';
* import { loadEnv } from 'payload/node';
*
* const port = 3001;
* loadEnv();
*
* const { default: config } = await import('@payload-config');
*
* const server = serve({
* fetch: async (request) => {
* const response = await handleEndpoints({
* config,
* request: request.clone(),
* });
*
* return response;
* },
* port,
* });
*
* server.on('listening', () => {
* console.log(`API server is listening on http://localhost:${port}/api`);
* });
* ```
*/ export const handleEndpoints = async ({ basePath = '', config: incomingConfig, path, request })=>{
let handler;
let req;
let collection;
// This can be used against GET request search params size limit.
// Instead you can do POST request with a text body as search params.
// We use this interally for relationships querying on the frontend
// packages/ui/src/fields/Relationship/index.tsx
if (request.method.toLowerCase() === 'post' && request.headers.get('X-HTTP-Method-Override') === 'GET') {
const search = await request.text();
const url = `${request.url}?${new URLSearchParams(search).toString()}`;
const response = await handleEndpoints({
basePath,
config: incomingConfig,
path,
request: new Request(url, {
cache: request.cache,
credentials: request.credentials,
headers: request.headers,
method: 'GET',
signal: request.signal
})
});
return response;
}
try {
req = await createPayloadRequest({
config: incomingConfig,
request
});
if (req.method.toLowerCase() === 'options') {
return Response.json({}, {
headers: headersWithCors({
headers: new Headers(),
req
}),
status: 200
});
}
const { payload } = req;
const { config } = payload;
const pathname = `${basePath}${path ?? new URL(req.url).pathname}`;
if (!pathname.startsWith(config.routes.api)) {
return notFoundResponse(req, pathname);
}
// /api/posts/route -> /posts/route
let adjustedPathname = pathname.replace(config.routes.api, '');
let isGlobals = false;
// /globals/header/route -> /header/route
if (adjustedPathname.startsWith('/globals')) {
isGlobals = true;
adjustedPathname = adjustedPathname.replace('/globals', '');
}
const segments = adjustedPathname.split('/');
// remove empty string first element
segments.shift();
const firstParam = segments[0];
let globalConfig;
// first param can be a global slug or collection slug, find the relevant config
if (firstParam) {
if (isGlobals) {
globalConfig = payload.globals.config.find((each)=>each.slug === firstParam);
} else if (payload.collections[firstParam]) {
collection = payload.collections[firstParam];
}
}
let endpoints = config.endpoints;
if (collection) {
endpoints = collection.config.endpoints;
// /posts/route -> /route
adjustedPathname = adjustedPathname.replace(`/${collection.config.slug}`, '');
} else if (globalConfig) {
// /header/route -> /route
adjustedPathname = adjustedPathname.replace(`/${globalConfig.slug}`, '');
endpoints = globalConfig.endpoints;
}
// sanitize when endpoint.path is '/'
if (adjustedPathname === '') {
adjustedPathname = '/';
}
if (endpoints === false) {
return Response.json({
message: `Cannot ${req.method.toUpperCase()} ${req.url}`
}, {
headers: headersWithCors({
headers: new Headers(),
req
}),
status: httpStatus.NOT_IMPLEMENTED
});
}
// Find the relevant endpoint configuration
const endpoint = endpoints?.find((endpoint)=>{
if (endpoint.method !== req.method.toLowerCase()) {
return false;
}
const pathMatchFn = match(endpoint.path, {
decode: decodeURIComponent
});
const matchResult = pathMatchFn(adjustedPathname);
if (!matchResult) {
return false;
}
req.routeParams = matchResult.params;
// Inject to routeParams the slug as well so it can be used later
if (collection) {
req.routeParams.collection = collection.config.slug;
} else if (globalConfig) {
req.routeParams.global = globalConfig.slug;
}
return true;
});
if (endpoint) {
handler = endpoint.handler;
}
if (!handler) {
return notFoundResponse(req, pathname);
}
const response = await handler(req);
return new Response(response.body, {
headers: headersWithCors({
headers: mergeHeaders(req.responseHeaders ?? new Headers(), response.headers),
req
}),
status: response.status,
statusText: response.statusText
});
} catch (err) {
return routeError({
collection,
config: incomingConfig,
err,
req
});
}
};
//# sourceMappingURL=handleEndpoints.js.map