Merge pull request #1161 from jacobsfletch/master
feat: supports root endpoints
This commit is contained in:
@@ -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 handlers 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:
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ export type CollectionConfig = {
|
||||
/**
|
||||
* Custom rest api endpoints
|
||||
*/
|
||||
endpoints?: Endpoint[]
|
||||
endpoints?: Omit<Endpoint, 'root'>[]
|
||||
/**
|
||||
* Access control
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -79,16 +79,19 @@ export type AccessResult = boolean | Where;
|
||||
*/
|
||||
export type Access = (args?: any) => AccessResult | Promise<AccessResult>;
|
||||
|
||||
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 }>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export type GlobalConfig = {
|
||||
beforeRead?: BeforeReadHook[]
|
||||
afterRead?: AfterReadHook[]
|
||||
}
|
||||
endpoints?: Endpoint[],
|
||||
endpoints?: Omit<Endpoint, 'root'>[],
|
||||
access?: {
|
||||
read?: Access;
|
||||
readDrafts?: Access;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { Response } from 'express';
|
||||
import express, { Response } from 'express';
|
||||
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,32 @@ 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: 'Hello, world!' });
|
||||
},
|
||||
},
|
||||
{
|
||||
path: `/${rootEndpoint}`,
|
||||
method: 'post',
|
||||
root: true,
|
||||
handler: [
|
||||
express.json({ type: 'application/json' }),
|
||||
(req: PayloadRequest, res: Response): void => {
|
||||
res.json(req.body);
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
@@ -87,4 +115,6 @@ export default buildConfig({
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default buildConfig(MyConfig);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { initPayloadTest } from '../helpers/configHelpers';
|
||||
import { RESTClient } from '../helpers/rest';
|
||||
import { applicationEndpoint, collectionSlug, globalEndpoint, globalSlug } from './config';
|
||||
import { applicationEndpoint, collectionSlug, globalEndpoint, globalSlug, rootEndpoint } from './config';
|
||||
|
||||
require('isomorphic-fetch');
|
||||
|
||||
@@ -15,21 +15,21 @@ describe('Endpoints', () => {
|
||||
|
||||
describe('Collections', () => {
|
||||
it('should GET a static endpoint', async () => {
|
||||
const { status, data } = await client.endpoint(`/${collectionSlug}/say-hello/joe-bloggs`);
|
||||
const { status, data } = await client.endpoint(`/api/${collectionSlug}/say-hello/joe-bloggs`);
|
||||
expect(status).toBe(200);
|
||||
expect(data.message).toStrictEqual('Hey Joey!');
|
||||
});
|
||||
|
||||
it('should GET an endpoint with a parameter', async () => {
|
||||
const name = 'George';
|
||||
const { status, data } = await client.endpoint(`/${collectionSlug}/say-hello/${name}`);
|
||||
const { status, data } = await client.endpoint(`/api/${collectionSlug}/say-hello/${name}`);
|
||||
expect(status).toBe(200);
|
||||
expect(data.message).toStrictEqual(`Hello ${name}!`);
|
||||
});
|
||||
|
||||
it('should POST an endpoint with data', async () => {
|
||||
const params = { name: 'George', age: 29 };
|
||||
const { status, data } = await client.endpoint(`/${collectionSlug}/whoami`, 'post', params);
|
||||
const { status, data } = await client.endpoint(`/api/${collectionSlug}/whoami`, 'post', params);
|
||||
expect(status).toBe(200);
|
||||
expect(data.name).toStrictEqual(params.name);
|
||||
expect(data.age).toStrictEqual(params.age);
|
||||
@@ -39,7 +39,7 @@ describe('Endpoints', () => {
|
||||
describe('Globals', () => {
|
||||
it('should call custom endpoint', async () => {
|
||||
const params = { globals: 'response' };
|
||||
const { status, data } = await client.endpoint(`/globals/${globalSlug}/${globalEndpoint}`, 'post', params);
|
||||
const { status, data } = await client.endpoint(`/api/globals/${globalSlug}/${globalEndpoint}`, 'post', params);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(params).toMatchObject(data);
|
||||
@@ -49,7 +49,17 @@ describe('Endpoints', () => {
|
||||
describe('API', () => {
|
||||
it('should call custom endpoint', async () => {
|
||||
const params = { app: 'response' };
|
||||
const { status, data } = await client.endpoint(`/${applicationEndpoint}`, 'post', params);
|
||||
const { status, data } = await client.endpoint(`/api/${applicationEndpoint}`, 'post', params);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(params).toMatchObject(data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Root', () => {
|
||||
it('should call custom root endpoint', async () => {
|
||||
const params = { root: 'response' };
|
||||
const { status, data } = await client.endpoint(`/${rootEndpoint}`, 'post', params);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(params).toMatchObject(data);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -255,8 +255,8 @@ export class RESTClient {
|
||||
return { status, doc: result };
|
||||
}
|
||||
|
||||
async endpoint<T = any>(path: string, method = 'get', params = undefined): Promise<{status: number, data: T}> {
|
||||
const response = await fetch(`${this.serverURL}/api${path}`, {
|
||||
async endpoint<T = any>(path: string, method = 'get', params = undefined): Promise<{ status: number, data: T }> {
|
||||
const response = await fetch(`${this.serverURL}${path}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user