diff --git a/docs/rest-api/overview.mdx b/docs/rest-api/overview.mdx index 57d8dad1db..7f4e3152e9 100644 --- a/docs/rest-api/overview.mdx +++ b/docs/rest-api/overview.mdx @@ -86,6 +86,7 @@ Each endpoint object needs to have: | **`path`** | A string for the endpoint route after the collection or globals slug | | **`method`** | The lowercase HTTP verb to use: 'get', 'head', 'post', 'put', 'delete', 'connect' or 'options' | | **`handler`** | A function or array of functions to be called with **req**, **res** and **next** arguments. [Express](https://expressjs.com/en/guide/routing.html#route-handlers) | +| **`root`** | When `true`, defines the endpoint on the root Express app, bypassing Payload handler and the `routes.api` subpath. Note: this only applies to top-level endpoints of your Payload config, endpoints defined on `collections` or `globals` cannot be root. | Example: diff --git a/src/collections/config/types.ts b/src/collections/config/types.ts index c8d4ec15b6..f9aa0c41b3 100644 --- a/src/collections/config/types.ts +++ b/src/collections/config/types.ts @@ -226,7 +226,7 @@ export type CollectionConfig = { /** * Custom rest api endpoints */ - endpoints?: Endpoint[] + endpoints?: Omit[] /** * Access control */ diff --git a/src/collections/init.ts b/src/collections/init.ts index fb94bdde98..736e42590d 100644 --- a/src/collections/init.ts +++ b/src/collections/init.ts @@ -113,7 +113,7 @@ export default function registerCollections(ctx: Payload): void { } const endpoints = buildEndpoints(collection); - mountEndpoints(router, endpoints); + mountEndpoints(ctx.express, router, endpoints); ctx.router.use(`/${slug}`, router); } diff --git a/src/config/schema.ts b/src/config/schema.ts index 14a815e876..83b8c6f47a 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -8,6 +8,7 @@ const component = joi.alternatives().try( export const endpointsSchema = joi.array().items(joi.object({ path: joi.string(), method: joi.string().valid('get', 'head', 'post', 'put', 'patch', 'delete', 'connect', 'options'), + root: joi.bool(), handler: joi.alternatives().try( joi.array().items(joi.func()), joi.func(), diff --git a/src/config/types.ts b/src/config/types.ts index 80291c03b6..e8517a6ac9 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -79,16 +79,19 @@ export type AccessResult = boolean | Where; */ export type Access = (args?: any) => AccessResult | Promise; -export interface PayloadHandler {( +export interface PayloadHandler { + ( req: PayloadRequest, res: Response, next: NextFunction, - ): void } + ): void +} export type Endpoint = { path: string method: 'get' | 'head' | 'post' | 'put' | 'patch' | 'delete' | 'connect' | 'options' | string handler: PayloadHandler | PayloadHandler[] + root?: boolean } export type AdminView = React.ComponentType<{ user: User, canAccessAdmin: boolean }> diff --git a/src/express/mountEndpoints.ts b/src/express/mountEndpoints.ts index eb0b547e5f..8ac026d461 100644 --- a/src/express/mountEndpoints.ts +++ b/src/express/mountEndpoints.ts @@ -1,9 +1,13 @@ -import { Router } from 'express'; +import { Express, Router } from 'express'; import { Endpoint } from '../config/types'; -function mountEndpoints(router: Router, endpoints: Endpoint[]): void { +function mountEndpoints(express: Express, router: Router, endpoints: Endpoint[]): void { endpoints.forEach((endpoint) => { - router[endpoint.method](endpoint.path, endpoint.handler); + if (!endpoint.root) { + router[endpoint.method](endpoint.path, endpoint.handler); + } else { + express[endpoint.method](endpoint.path, endpoint.handler); + } }); } diff --git a/src/globals/config/types.ts b/src/globals/config/types.ts index c46d8de9ab..14a14a0552 100644 --- a/src/globals/config/types.ts +++ b/src/globals/config/types.ts @@ -56,7 +56,7 @@ export type GlobalConfig = { beforeRead?: BeforeReadHook[] afterRead?: AfterReadHook[] } - endpoints?: Endpoint[], + endpoints?: Omit[], access?: { read?: Access; readDrafts?: Access; diff --git a/src/globals/init.ts b/src/globals/init.ts index e8732291bd..a82d946d92 100644 --- a/src/globals/init.ts +++ b/src/globals/init.ts @@ -48,7 +48,7 @@ export default function initGlobals(ctx: Payload): void { const { slug } = global; const endpoints = buildEndpoints(global); - mountEndpoints(router, endpoints); + mountEndpoints(ctx.express, router, endpoints); ctx.router.use(`/globals/${slug}`, router); }); diff --git a/src/init.ts b/src/init.ts index 1f5d7daeb8..2a3dd916fb 100644 --- a/src/init.ts +++ b/src/init.ts @@ -106,7 +106,7 @@ export const init = (payload: Payload, options: InitOptions): void => { initGraphQLPlayground(payload); } - mountEndpoints(payload.router, payload.config.endpoints); + mountEndpoints(options.express, payload.router, payload.config.endpoints); // Bind router to API payload.express.use(payload.config.routes.api, payload.router); diff --git a/test/admin/payload-types.ts b/test/admin/payload-types.ts index b1e2c80c45..fa4d331688 100644 --- a/test/admin/payload-types.ts +++ b/test/admin/payload-types.ts @@ -16,14 +16,19 @@ export interface Global { } /** * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "posts". + * via the `definition` "group-globals-one". */ -export interface Post { +export interface GroupGlobalsOne { + id: string; + title?: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "group-globals-two". + */ +export interface GroupGlobalsTwo { id: string; title?: string; - description?: string; - createdAt: string; - updatedAt: string; } /** * This interface was referenced by `Config`'s JSON-Schema @@ -39,3 +44,55 @@ export interface User { createdAt: string; updatedAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "posts". + */ +export interface Post { + id: string; + title?: string; + description?: string; + number?: number; + createdAt: string; + updatedAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "group-one-collection-ones". + */ +export interface GroupOneCollectionOne { + id: string; + title?: string; + createdAt: string; + updatedAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "group-one-collection-twos". + */ +export interface GroupOneCollectionTwo { + id: string; + title?: string; + createdAt: string; + updatedAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "group-two-collection-ones". + */ +export interface GroupTwoCollectionOne { + id: string; + title?: string; + createdAt: string; + updatedAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "group-two-collection-twos". + */ +export interface GroupTwoCollectionTwo { + id: string; + title?: string; + createdAt: string; + updatedAt: string; +} diff --git a/test/endpoints/config.ts b/test/endpoints/config.ts index 171a3690c2..23e9ca3869 100644 --- a/test/endpoints/config.ts +++ b/test/endpoints/config.ts @@ -3,14 +3,16 @@ import { devUser } from '../credentials'; import { buildConfig } from '../buildConfig'; import { openAccess } from '../helpers/configHelpers'; import { PayloadRequest } from '../../src/express/types'; +import { Config } from '../../src/config/types'; export const collectionSlug = 'endpoints'; export const globalSlug = 'global-endpoints'; export const globalEndpoint = 'global'; export const applicationEndpoint = 'path'; +export const rootEndpoint = 'root'; -export default buildConfig({ +const MyConfig: Config = { collections: [ { slug: collectionSlug, @@ -77,6 +79,21 @@ export default buildConfig({ res.json(req.body); }, }, + { + path: `/${applicationEndpoint}`, + method: 'get', + handler: (req: PayloadRequest, res: Response): void => { + res.json({ message: 'Hello, world!' }); + }, + }, + { + path: `/${rootEndpoint}`, + method: 'get', + root: true, + handler: (req: PayloadRequest, res: Response): void => { + res.json({ message: 'Root.' }); + }, + }, ], onInit: async (payload) => { await payload.create({ @@ -87,4 +104,6 @@ export default buildConfig({ }, }); }, -}); +} + +export default buildConfig(MyConfig); diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index a10e766fe8..8bba442033 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -16,6 +16,10 @@ export interface ArrayField { text: string; id?: string; }[]; + collapsedArray: { + text: string; + id?: string; + }[]; localized: { text: string; id?: string; @@ -80,6 +84,49 @@ export interface BlockField { blockType: 'tabs'; } )[]; + collapsedByDefaultBlocks: ( + | { + text: string; + richText?: { + [k: string]: unknown; + }[]; + id?: string; + blockName?: string; + blockType: 'text'; + } + | { + number: number; + id?: string; + blockName?: string; + blockType: 'number'; + } + | { + subBlocks: ( + | { + text: string; + id?: string; + blockName?: string; + blockType: 'text'; + } + | { + number: number; + id?: string; + blockName?: string; + blockType: 'number'; + } + )[]; + id?: string; + blockName?: string; + blockType: 'subBlocks'; + } + | { + textInCollapsible?: string; + textInRow?: string; + id?: string; + blockName?: string; + blockType: 'tabs'; + } + )[]; localizedBlocks: ( | { text: string; @@ -153,6 +200,7 @@ export interface CollapsibleField { textWithinSubGroup?: string; }; }; + someText?: string; createdAt: string; updatedAt: string; }