Custom collection/global endpoints (#607)
* feat: add custom endpoints for collections and globals REST APIs Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
This commit is contained in:
61
demo/collections/Endpoints.ts
Normal file
61
demo/collections/Endpoints.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Response } from 'express';
|
||||
import { CollectionConfig } from '../../src/collections/config/types';
|
||||
import { PayloadRequest } from '../../src/express/types';
|
||||
|
||||
const Endpoints: CollectionConfig = {
|
||||
slug: 'endpoints',
|
||||
labels: {
|
||||
singular: 'Endpoint',
|
||||
plural: 'Endpoints',
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
access: {
|
||||
create: () => true,
|
||||
read: () => true,
|
||||
update: () => true,
|
||||
delete: () => true,
|
||||
},
|
||||
endpoints: [
|
||||
{
|
||||
path: '/say-hello/joe-bloggs',
|
||||
method: 'get',
|
||||
handler: (req: PayloadRequest, res: Response): void => {
|
||||
res.json({ message: `Hey Joey! Welcome to ${req.payload.getAPIURL()}` });
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/say-hello/:group/:name',
|
||||
method: 'get',
|
||||
handler: (req: PayloadRequest, res: Response): void => {
|
||||
res.json({ message: `Hello ${req.params.name} @ ${req.params.group}` });
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/say-hello/:name',
|
||||
method: 'get',
|
||||
handler: (req: PayloadRequest, res: Response): void => {
|
||||
res.json({ message: `Hello ${req.params.name}!` });
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/whoami',
|
||||
method: 'post',
|
||||
handler: (req: PayloadRequest, res: Response): void => {
|
||||
res.json({
|
||||
name: req.body.name,
|
||||
age: req.body.age,
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default Endpoints;
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Response } from 'express';
|
||||
import { GlobalConfig } from '../../src/globals/config/types';
|
||||
import checkRole from '../access/checkRole';
|
||||
import { NavigationArray as TNavigationArray } from '../payload-types';
|
||||
import { PayloadRequest } from '../../src/express/types';
|
||||
|
||||
const NavigationArray: GlobalConfig = {
|
||||
slug: 'navigation-array',
|
||||
@@ -10,6 +13,19 @@ const NavigationArray: GlobalConfig = {
|
||||
admin: {
|
||||
description: 'A description for the editor',
|
||||
},
|
||||
endpoints: [
|
||||
{
|
||||
path: '/count',
|
||||
method: 'get',
|
||||
handler: async (req: PayloadRequest, res: Response): Promise<void> => {
|
||||
const { array } = await req.payload.findGlobal<TNavigationArray>({
|
||||
slug: 'navigation-array',
|
||||
});
|
||||
|
||||
res.json({ count: array.length });
|
||||
},
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
name: 'array',
|
||||
|
||||
@@ -147,6 +147,10 @@ export interface AllFields {
|
||||
checkbox?: boolean;
|
||||
id?: string;
|
||||
}[];
|
||||
readOnlyArray?: {
|
||||
text?: string;
|
||||
id?: string;
|
||||
}[];
|
||||
blocks: (
|
||||
| {
|
||||
testEmail: string;
|
||||
@@ -283,6 +287,7 @@ export interface Conditions {
|
||||
blockType: 'cta';
|
||||
}
|
||||
)[];
|
||||
customComponent: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
@@ -482,6 +487,14 @@ export interface DefaultValueTest {
|
||||
asyncText?: string;
|
||||
function?: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "endpoints".
|
||||
*/
|
||||
export interface Endpoint {
|
||||
id: string;
|
||||
title?: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "files".
|
||||
|
||||
@@ -8,6 +8,7 @@ import Autosave from './collections/Autosave';
|
||||
import Code from './collections/Code';
|
||||
import Conditions from './collections/Conditions';
|
||||
// import CustomComponents from './collections/CustomComponents';
|
||||
import Endpoints from './collections/Endpoints';
|
||||
import File from './collections/File';
|
||||
import Blocks from './collections/Blocks';
|
||||
import CustomID from './collections/CustomID';
|
||||
@@ -100,6 +101,7 @@ export default buildConfig({
|
||||
// CustomComponents,
|
||||
CustomID,
|
||||
DefaultValues,
|
||||
Endpoints,
|
||||
File,
|
||||
Geolocation,
|
||||
HiddenFields,
|
||||
|
||||
@@ -25,6 +25,7 @@ It's often best practice to write your Collections in separate files and then im
|
||||
| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](/docs/upload/overview) documentation. |
|
||||
| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
|
||||
| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#collection-config)|
|
||||
| **`endpoints`** | Add custom routes to the REST API. [More](/docs/rest-api#custom-endpoints) |
|
||||
|
||||
*\* An asterisk denotes that a property is required.*
|
||||
|
||||
@@ -80,25 +81,25 @@ If the function is specified, a Preview button will automatically appear in the
|
||||
**Example collection with preview function:**
|
||||
|
||||
```js
|
||||
{
|
||||
const Posts = {
|
||||
slug: 'posts',
|
||||
fields: [
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
required: true,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
admin: {
|
||||
preview: (doc, { locale }) => {
|
||||
if (doc?.slug) {
|
||||
return `https://bigbird.com/preview/posts/${doc.slug}?locale=${locale}`,
|
||||
return `https://bigbird.com/preview/posts/${doc.slug}?locale=${locale}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Access control
|
||||
|
||||
@@ -22,6 +22,7 @@ As with Collection configs, it's often best practice to write your Globals in se
|
||||
| **`hooks`** | Entry points to "tie in" to collection actions at specific points. [More](/docs/hooks/overview#global-hooks) |
|
||||
| **`access`** | Provide access control functions to define exactly who should be able to do what with this Global. [More](/docs/access-control/overview/#globals) |
|
||||
| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#globals-config)|
|
||||
| **`endpoints`** | Add custom routes to the REST API. [More](/docs/rest-api#custom-endpoints)|
|
||||
|
||||
*\* An asterisk denotes that a property is required.*
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ The `find` endpoint supports the following additional query parameters:
|
||||
|
||||
## Auth Operations
|
||||
|
||||
Auth enabled collections are also given the following endpoints:
|
||||
|
||||
| Method | Path | Description |
|
||||
| -------- | --------------------------- | ----------- |
|
||||
| `POST` | `/api/{collectionSlug}/verify/:token` | [Email verification](/docs/authentication/operations#verify-by-email), if enabled. |
|
||||
@@ -72,3 +74,47 @@ In addition to the dynamically generated endpoints above Payload also has REST e
|
||||
| `GET` | `/api/_preferences/{key}` | Get a preference by key |
|
||||
| `POST` | `/api/_preferences/{key}` | Create or update by key |
|
||||
| `DELETE` | `/api/_preferences/{key}` | Delete a user preference by key |
|
||||
|
||||
## Custom Endpoints
|
||||
|
||||
Additional REST API endpoints can be added to `collections` and `globals` by providing array of `endpoints` in the configuration. These can be used to write additional middleware on existing routes or build custom functionality into Payload apps and plugins.
|
||||
|
||||
Each endpoint object needs to have:
|
||||
|
||||
| Property | Description |
|
||||
| ---------- | ----------------------------------------- |
|
||||
| **`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) |
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
|
||||
// a collection of 'orders' with an additional route for tracking details, reachable at /api/orders/:id/tracking
|
||||
const Orders = {
|
||||
slug: 'orders',
|
||||
fields: [ /* ... */ ],
|
||||
// highlight-start
|
||||
endpoints: [
|
||||
{
|
||||
path: '/:id/tracking',
|
||||
method: 'get',
|
||||
handler: async (req, res, next) => {
|
||||
const tracking = await getTrackingInfo(req.params.id);
|
||||
if (tracking) {
|
||||
res.status('200').send({ tracking });
|
||||
} else {
|
||||
res.status('404').send({ error: 'not found' });
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
// highlight-end
|
||||
}
|
||||
```
|
||||
|
||||
<Banner>
|
||||
<strong>Note:</strong><br/>
|
||||
**req** will have the **payload** object and can be used inside your endpoint handlers for making calls like req.payload.find() that will make use of access control and hooks.
|
||||
</Banner>
|
||||
|
||||
@@ -3,7 +3,9 @@ import { PayloadRequest } from '../../express/types';
|
||||
|
||||
export default async function me(req: PayloadRequest, res: Response, next: NextFunction): Promise<any> {
|
||||
try {
|
||||
const collectionSlug = req.route.path.split('/').filter((r) => r !== '')[0];
|
||||
const collectionSlugMatch = req.originalUrl.match(/\/([^/]+)\/me\/?$/);
|
||||
const [, collectionSlug] = collectionSlugMatch;
|
||||
|
||||
const response = await this.operations.collections.auth.me({
|
||||
req,
|
||||
collectionSlug,
|
||||
|
||||
@@ -32,6 +32,7 @@ export const defaults = {
|
||||
afterLogin: [],
|
||||
afterForgotPassword: [],
|
||||
},
|
||||
endpoints: [],
|
||||
auth: false,
|
||||
upload: false,
|
||||
versions: false,
|
||||
|
||||
@@ -53,6 +53,14 @@ const collectionSchema = joi.object().keys({
|
||||
afterLogin: joi.array().items(joi.func()),
|
||||
afterForgotPassword: joi.array().items(joi.func()),
|
||||
}),
|
||||
endpoints: joi.array().items(joi.object({
|
||||
path: joi.string(),
|
||||
method: joi.string().valid('get', 'head', 'post', 'put', 'patch', 'delete', 'connect', 'options'),
|
||||
handler: joi.alternatives().try(
|
||||
joi.array().items(joi.func()),
|
||||
joi.func(),
|
||||
),
|
||||
})),
|
||||
auth: joi.alternatives().try(
|
||||
joi.object({
|
||||
tokenExpiration: joi.number(),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { DeepRequired } from 'ts-essentials';
|
||||
import { PaginateModel } from 'mongoose';
|
||||
import { GraphQLType } from 'graphql';
|
||||
import { Access, GeneratePreviewURL, EntityDescription } from '../../config/types';
|
||||
import { Access, GeneratePreviewURL, EntityDescription, Endpoint } from '../../config/types';
|
||||
import { Field } from '../../fields/config/types';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { IncomingAuthType, Auth } from '../../auth/types';
|
||||
@@ -193,6 +193,10 @@ export type CollectionConfig = {
|
||||
afterLogin?: AfterLoginHook[];
|
||||
afterForgotPassword?: AfterForgotPasswordHook[];
|
||||
};
|
||||
/**
|
||||
* Custom rest api endpoints
|
||||
*/
|
||||
endpoints?: Endpoint[]
|
||||
/**
|
||||
* Access control
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,7 @@ import bindCollectionMiddleware from './bindCollection';
|
||||
import { CollectionModel, SanitizedCollectionConfig } from './config/types';
|
||||
import { Payload } from '../index';
|
||||
import { getVersionsModelName } from '../versions/getVersionsModelName';
|
||||
import mountEndpoints from '../init/mountEndpoints';
|
||||
|
||||
const LocalStrategy = Passport.Strategy;
|
||||
|
||||
@@ -95,9 +96,11 @@ export default function registerCollections(ctx: Payload): void {
|
||||
// If not local, open routes
|
||||
if (!ctx.local) {
|
||||
const router = express.Router();
|
||||
const { slug } = collection;
|
||||
const { slug, endpoints } = collection;
|
||||
|
||||
router.all(`/${slug}*`, bindCollectionMiddleware(ctx.collections[formattedCollection.slug]));
|
||||
router.all('*', bindCollectionMiddleware(ctx.collections[formattedCollection.slug]));
|
||||
|
||||
mountEndpoints(router, endpoints);
|
||||
|
||||
const {
|
||||
create,
|
||||
@@ -133,68 +136,68 @@ export default function registerCollections(ctx: Payload): void {
|
||||
|
||||
if (collection.auth.verify) {
|
||||
router
|
||||
.route(`/${slug}/verify/:token`)
|
||||
.route('/verify/:token')
|
||||
.post(verifyEmail);
|
||||
}
|
||||
|
||||
if (collection.auth.maxLoginAttempts > 0) {
|
||||
router
|
||||
.route(`/${slug}/unlock`)
|
||||
.route('/unlock')
|
||||
.post(unlock);
|
||||
}
|
||||
|
||||
router
|
||||
.route(`/${slug}/init`)
|
||||
.route('/init')
|
||||
.get(init);
|
||||
|
||||
router
|
||||
.route(`/${slug}/login`)
|
||||
.route('/login')
|
||||
.post(login);
|
||||
|
||||
router
|
||||
.route(`/${slug}/logout`)
|
||||
.route('/logout')
|
||||
.post(logout);
|
||||
|
||||
router
|
||||
.route(`/${slug}/refresh-token`)
|
||||
.route('/refresh-token')
|
||||
.post(refresh);
|
||||
|
||||
router
|
||||
.route(`/${slug}/me`)
|
||||
.route('/me')
|
||||
.get(me);
|
||||
|
||||
router
|
||||
.route(`/${slug}/first-register`)
|
||||
.route('/first-register')
|
||||
.post(registerFirstUser);
|
||||
|
||||
router
|
||||
.route(`/${slug}/forgot-password`)
|
||||
.route('/forgot-password')
|
||||
.post(forgotPassword);
|
||||
|
||||
router
|
||||
.route(`/${slug}/reset-password`)
|
||||
.route('/reset-password')
|
||||
.post(resetPassword);
|
||||
}
|
||||
|
||||
if (collection.versions) {
|
||||
router.route(`/${slug}/versions`)
|
||||
router.route('/versions')
|
||||
.get(findVersions);
|
||||
|
||||
router.route(`/${slug}/versions/:id`)
|
||||
router.route('/versions/:id')
|
||||
.get(findVersionByID)
|
||||
.post(restoreVersion);
|
||||
}
|
||||
|
||||
router.route(`/${slug}`)
|
||||
router.route('/')
|
||||
.get(find)
|
||||
.post(create);
|
||||
|
||||
router.route(`/${slug}/:id`)
|
||||
router.route('/:id')
|
||||
.put(update)
|
||||
.get(findByID)
|
||||
.delete(deleteHandler);
|
||||
|
||||
ctx.router.use(router);
|
||||
ctx.router.use(`/${slug}`, router);
|
||||
}
|
||||
|
||||
return formattedCollection;
|
||||
|
||||
73
src/collections/tests/endpoints.spec.ts
Normal file
73
src/collections/tests/endpoints.spec.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import getConfig from '../../config/load';
|
||||
import { email, password } from '../../mongoose/testCredentials';
|
||||
|
||||
require('isomorphic-fetch');
|
||||
|
||||
const { serverURL: url } = getConfig();
|
||||
|
||||
let token = null;
|
||||
let headers = null;
|
||||
|
||||
describe('Collections - REST', () => {
|
||||
beforeAll(async (done) => {
|
||||
const response = await fetch(`${url}/api/admins/login`, {
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
({ token } = data);
|
||||
headers = {
|
||||
Authorization: `JWT ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
describe('Endpoints', () => {
|
||||
it('should GET a static endpoint', async () => {
|
||||
const response = await fetch(`${url}/api/endpoints/say-hello/joe-bloggs`, {
|
||||
headers: {
|
||||
...headers,
|
||||
},
|
||||
method: 'get',
|
||||
});
|
||||
const data = await response.json();
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.message).toStrictEqual(`Hey Joey! Welcome to ${url}/api`);
|
||||
});
|
||||
it('should GET an endpoint with a parameter', async () => {
|
||||
const response = await fetch(`${url}/api/endpoints/say-hello/George`, {
|
||||
headers: {
|
||||
...headers,
|
||||
},
|
||||
method: 'get',
|
||||
});
|
||||
const data = await response.json();
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.message).toStrictEqual('Hello George!');
|
||||
});
|
||||
it('should POST an endpoint with data', async () => {
|
||||
const params = { name: 'George', age: 29 };
|
||||
const response = await fetch(`${url}/api/endpoints/whoami`, {
|
||||
body: JSON.stringify(params),
|
||||
headers: {
|
||||
...headers,
|
||||
},
|
||||
method: 'post',
|
||||
});
|
||||
const data = await response.json();
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.name).toStrictEqual(params.name);
|
||||
expect(data.age).toStrictEqual(params.age);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Express } from 'express';
|
||||
import { Express, Handler } from 'express';
|
||||
import { DeepRequired } from 'ts-essentials';
|
||||
import { Transporter } from 'nodemailer';
|
||||
import { Options } from 'express-fileupload';
|
||||
@@ -79,6 +79,12 @@ export type AccessResult = boolean | Where;
|
||||
*/
|
||||
export type Access = (args?: any) => AccessResult | Promise<AccessResult>;
|
||||
|
||||
export type Endpoint = {
|
||||
path: string
|
||||
method: 'get' | 'head' | 'post' | 'put' | 'patch' | 'delete' | 'connect' | 'options' | string
|
||||
handler: Handler | Handler[]
|
||||
}
|
||||
|
||||
export type AdminView = React.ComponentType<{ user: User, canAccessAdmin: boolean }>
|
||||
|
||||
export type AdminRoute = {
|
||||
|
||||
@@ -24,7 +24,6 @@ const validateFields = (context: string, entity: SanitizedCollectionConfig | San
|
||||
});
|
||||
}
|
||||
if (result.error) {
|
||||
console.log(result.error);
|
||||
result.error.details.forEach(({ message }) => {
|
||||
errors.push(`${context} "${entity.slug}" > Field${fieldAffectsData(field) ? ` "${field.name}" >` : ''} ${message}`);
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ const sanitizeGlobals = (collections: CollectionConfig[], globals: GlobalConfig[
|
||||
// /////////////////////////////////
|
||||
|
||||
if (!sanitizedGlobal.hooks) sanitizedGlobal.hooks = {};
|
||||
if (!sanitizedGlobal.endpoints) sanitizedGlobal.endpoints = [];
|
||||
if (!sanitizedGlobal.access) sanitizedGlobal.access = {};
|
||||
if (!sanitizedGlobal.admin) sanitizedGlobal.admin = {};
|
||||
|
||||
|
||||
@@ -18,6 +18,14 @@ const globalSchema = joi.object().keys({
|
||||
beforeRead: joi.array().items(joi.func()),
|
||||
afterRead: joi.array().items(joi.func()),
|
||||
}),
|
||||
endpoints: joi.array().items(joi.object({
|
||||
path: joi.string(),
|
||||
method: joi.string().valid('get', 'head', 'post', 'put', 'patch', 'delete', 'connect', 'options'),
|
||||
handler: joi.alternatives().try(
|
||||
joi.array().items(joi.func()),
|
||||
joi.func(),
|
||||
),
|
||||
})),
|
||||
access: joi.object({
|
||||
read: joi.func(),
|
||||
readVersions: joi.func(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { Model, Document } from 'mongoose';
|
||||
import { DeepRequired } from 'ts-essentials';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { Access, GeneratePreviewURL } from '../../config/types';
|
||||
import { Access, Endpoint, GeneratePreviewURL } from '../../config/types';
|
||||
import { Field } from '../../fields/config/types';
|
||||
import { IncomingGlobalVersions, SanitizedGlobalVersions } from '../../versions/types';
|
||||
|
||||
@@ -56,6 +56,7 @@ export type GlobalConfig = {
|
||||
beforeRead?: BeforeReadHook[]
|
||||
afterRead?: AfterReadHook[]
|
||||
}
|
||||
endpoints?: Endpoint[],
|
||||
access?: {
|
||||
read?: Access;
|
||||
readDrafts?: Access;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getVersionsModelName } from '../versions/getVersionsModelName';
|
||||
import { buildVersionGlobalFields } from '../versions/buildGlobalFields';
|
||||
import buildSchema from '../mongoose/buildSchema';
|
||||
import { CollectionModel } from '../collections/config/types';
|
||||
import mountEndpoints from '../init/mountEndpoints';
|
||||
|
||||
export default function initGlobals(ctx: Payload): void {
|
||||
if (ctx.config.globals) {
|
||||
@@ -40,25 +41,28 @@ export default function initGlobals(ctx: Payload): void {
|
||||
|
||||
// If not local, open routes
|
||||
if (!ctx.local) {
|
||||
const router = express.Router();
|
||||
|
||||
ctx.config.globals.forEach((global) => {
|
||||
const router = express.Router();
|
||||
const { slug, endpoints } = global;
|
||||
|
||||
mountEndpoints(router, endpoints);
|
||||
|
||||
router
|
||||
.route(`/globals/${global.slug}`)
|
||||
.route('/')
|
||||
.get(ctx.requestHandlers.globals.findOne(global))
|
||||
.post(ctx.requestHandlers.globals.update(global));
|
||||
|
||||
if (global.versions) {
|
||||
router.route(`/globals/${global.slug}/versions`)
|
||||
router.route('/versions')
|
||||
.get(ctx.requestHandlers.globals.findVersions(global));
|
||||
|
||||
router.route(`/globals/${global.slug}/versions/:id`)
|
||||
router.route('/versions/:id')
|
||||
.get(ctx.requestHandlers.globals.findVersionByID(global))
|
||||
.post(ctx.requestHandlers.globals.restoreVersion(global));
|
||||
}
|
||||
});
|
||||
|
||||
ctx.router.use(router);
|
||||
ctx.router.use(`/globals/${slug}`, router);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,4 +174,19 @@ describe('Globals - REST', () => {
|
||||
expect(textarea).toStrictEqual(navData.es.nav1.textarea);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Endpoints', () => {
|
||||
it('should respond with number of navigation items', async () => {
|
||||
const response = await fetch(`${url}/api/globals/navigation-array/count`, {
|
||||
headers: {
|
||||
Authorization: `JWT ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const data = await response.json();
|
||||
expect(data.count).toStrictEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
10
src/init/mountEndpoints.ts
Normal file
10
src/init/mountEndpoints.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Router } from 'express';
|
||||
import { Endpoint } from '../config/types';
|
||||
|
||||
function mountEndpoints(router: Router, endpoints: Endpoint[]): void {
|
||||
endpoints.forEach((endpoint) => {
|
||||
router[endpoint.method](endpoint.path, endpoint.handler);
|
||||
});
|
||||
}
|
||||
|
||||
export default mountEndpoints;
|
||||
Reference in New Issue
Block a user