Merge branch 'master' into fix/read-only
This commit is contained in:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,3 +1,16 @@
|
|||||||
|
## [1.1.4](https://github.com/payloadcms/payload/compare/v1.1.3...v1.1.4) (2022-09-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* field level access for nested fields ([22ea98c](https://github.com/payloadcms/payload/commit/22ea98ca33770a0ec6652f814726454abb6da24e))
|
||||||
|
* refine type generation for relationships ([ef83bdb](https://github.com/payloadcms/payload/commit/ef83bdb709ebde008b90930a6875b24f042a41b0))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* supports root endpoints ([52cd3b4](https://github.com/payloadcms/payload/commit/52cd3b4a7ed9bc85e93d753a3aaf190489ca98cd))
|
||||||
|
|
||||||
## [1.1.3](https://github.com/payloadcms/payload/compare/v1.1.2...v1.1.3) (2022-09-16)
|
## [1.1.3](https://github.com/payloadcms/payload/compare/v1.1.2...v1.1.3) (2022-09-16)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ The directory split up in this way specifically to reduce friction when creating
|
|||||||
|
|
||||||
The following command will start Payload with your config: `yarn dev my-test-dir`. This command will start up Payload using your config and refresh a test database on every restart.
|
The following command will start Payload with your config: `yarn dev my-test-dir`. This command will start up Payload using your config and refresh a test database on every restart.
|
||||||
|
|
||||||
NOTE: It is recommended to add the test credentials to your autofill for `localhost:3000/admin` as this will be required on every nodemon restart.
|
When switching between test directories, you will want to remove your `node_modules/.cache ` manually or by running `yarn clean:cache`.
|
||||||
|
|
||||||
|
NOTE: It is recommended to add the test credentials (located in `test/credentials.ts`) to your autofill for `localhost:3000/admin` as this will be required on every nodemon restart.
|
||||||
|
|
||||||
## Pull Requests
|
## Pull Requests
|
||||||
|
|
||||||
|
|||||||
@@ -97,11 +97,51 @@ All Payload fields support the ability to swap in your own React components. So,
|
|||||||
|
|
||||||
**Fields support the following custom components:**
|
**Fields support the following custom components:**
|
||||||
|
|
||||||
| Component | Description |
|
| Component | Description |
|
||||||
| --------------- | -------------|
|
| --------------- |------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| **`Filter`** | Override the text input that is presented in the `List` view when a user is filtering documents by the customized field. |
|
| **`Filter`** | Override the text input that is presented in the `List` view when a user is filtering documents by the customized field. |
|
||||||
| **`Cell`** | Used in the `List` view's table to represent a table-based preview of the data stored in the field. |
|
| **`Cell`** | Used in the `List` view's table to represent a table-based preview of the data stored in the field. [More](#cell-component) |
|
||||||
| **`Field`** | Swap out the field itself within all `Edit` views. |
|
| **`Field`** | Swap out the field itself within all `Edit` views. [More](#field-component) |
|
||||||
|
|
||||||
|
## Cell Component
|
||||||
|
|
||||||
|
These are the props that will be passed to your custom Cell to use in your own components.
|
||||||
|
|
||||||
|
| Property | Description |
|
||||||
|
|--------------|-------------------------------------------------------------------|
|
||||||
|
| **`field`** | An object that includes the field configuration. |
|
||||||
|
| **`colIndex`** | A unique number for the column in the list. |
|
||||||
|
| **`collection`** | An object with the config of the collection that the field is in. |
|
||||||
|
| **`cellData`** | The data for the field that the cell represents. |
|
||||||
|
| **`rowData`** | An object with all the field values for the row. |
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import React from 'react';
|
||||||
|
import './index.scss';
|
||||||
|
const baseClass = 'custom-cell';
|
||||||
|
|
||||||
|
const CustomCell: React.FC<Props> = (props) => {
|
||||||
|
const {
|
||||||
|
field,
|
||||||
|
colIndex,
|
||||||
|
collection,
|
||||||
|
cellData,
|
||||||
|
rowData,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={baseClass}>
|
||||||
|
{ cellData }
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Field Component
|
||||||
|
|
||||||
|
When writing your own custom components you can make use of a number of hooks to set data, get reactive changes to other fields, get the id of the document or interact with a context from a custom provider.
|
||||||
|
|
||||||
### Sending and receiving values from the form
|
### Sending and receiving values from the form
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,11 @@ export default {};
|
|||||||
|
|
||||||
Now, when Webpack sees that you're attempting to import your `createStripeSubscriptionPath` file, it'll disregard that actual file and load your mock file instead. Not only will your Admin panel now bundle successfully, you will have optimized its filesize by removing unnecessary code! And you might have learned something about Webpack, too.
|
Now, when Webpack sees that you're attempting to import your `createStripeSubscriptionPath` file, it'll disregard that actual file and load your mock file instead. Not only will your Admin panel now bundle successfully, you will have optimized its filesize by removing unnecessary code! And you might have learned something about Webpack, too.
|
||||||
|
|
||||||
|
<Banner type="success">
|
||||||
|
<strong>Tip:</strong><br/>
|
||||||
|
If changes to your Webpack aliases are not surfacing, they might be [cached](https://webpack.js.org/configuration/cache/) in `node_modules/.cache/webpack`. Try deleting that folder and restarting your server.
|
||||||
|
</Banner>
|
||||||
|
|
||||||
## Admin environment vars
|
## Admin environment vars
|
||||||
|
|
||||||
<Banner type="warning">
|
<Banner type="warning">
|
||||||
|
|||||||
@@ -174,6 +174,20 @@ const ExampleCollection: CollectionConfig = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
link: {
|
||||||
|
// Inject your own fields into the Link element
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'rel',
|
||||||
|
label: 'Rel Attribute',
|
||||||
|
type: 'select',
|
||||||
|
hasMany: true,
|
||||||
|
options: [
|
||||||
|
'noopener', 'noreferrer', 'nofollow',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
upload: {
|
upload: {
|
||||||
collections: {
|
collections: {
|
||||||
media: {
|
media: {
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ With this field, you can also inject custom `Cell` components that appear as add
|
|||||||
|
|
||||||
### Config
|
### Config
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
| ---------------------------- | ----------- |
|
| ---------------------------- |-------------------------------------------------------------------------------------------------------------------|
|
||||||
| **`name`** * | A unique identifier for this field. |
|
| **`name`** * | A unique identifier for this field. |
|
||||||
| **`label`** | Human-readable label for this UI field. |
|
| **`label`** | Human-readable label for this UI field. |
|
||||||
| **`admin.components.Field`** | React component to be rendered for this field within the Edit view. |
|
| **`admin.components.Field`** | React component to be rendered for this field within the Edit view. [More](/admin/components/#field-component) |
|
||||||
| **`admin.components.Cell`** | React component to be rendered as a Cell within collection List views. |
|
| **`admin.components.Cell`** | React component to be rendered as a Cell within collection List views. [More](/admin/components/#field-component) |
|
||||||
|
|
||||||
*\* An asterisk denotes that a property is required.*
|
*\* An asterisk denotes that a property is required.*
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Writing plugins is no more complex than writing regular JavaScript. If you know
|
|||||||
|
|
||||||
### How to install plugins
|
### How to install plugins
|
||||||
|
|
||||||
The base Payload config allows for a `plugins` property which takes an `array` of [`Plugin`s](https://github.com/payloadcms/payload/blob/master/src/config/types.ts#L21).
|
The base Payload config allows for a `plugins` property which takes an `array` of [`Plugins`](https://github.com/payloadcms/payload/blob/master/src/config/types.ts#L21).
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { buildConfig } from 'payload/config';
|
import { buildConfig } from 'payload/config';
|
||||||
@@ -134,7 +134,7 @@ const addLastModified: Plugin = (incomingConfig: Config): Config => {
|
|||||||
export default addLastModified;
|
export default addLastModified;
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Available Plugins
|
### Available Plugins
|
||||||
|
|
||||||
You can discover existing plugins by browsing the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin).
|
You can discover existing plugins by browsing the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin).
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ Each endpoint object needs to have:
|
|||||||
| **`path`** | A string for the endpoint route after the collection or globals slug |
|
| **`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' |
|
| **`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) |
|
| **`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:
|
Example:
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "payload",
|
"name": "payload",
|
||||||
"version": "1.1.3",
|
"version": "1.1.4",
|
||||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
"test:e2e:headed": "cross-env DISABLE_LOGGING=true playwright test --headed",
|
"test:e2e:headed": "cross-env DISABLE_LOGGING=true playwright test --headed",
|
||||||
"test:e2e:debug": "cross-env PWDEBUG=1 DISABLE_LOGGING=true playwright test",
|
"test:e2e:debug": "cross-env PWDEBUG=1 DISABLE_LOGGING=true playwright test",
|
||||||
"test:components": "cross-env jest --config=jest.components.config.js",
|
"test:components": "cross-env jest --config=jest.components.config.js",
|
||||||
|
"clean:cache": "rimraf node_modules/.cache",
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"release": "release-it",
|
"release": "release-it",
|
||||||
"release:patch": "release-it patch",
|
"release:patch": "release-it patch",
|
||||||
@@ -169,7 +170,7 @@
|
|||||||
"react-sortable-hoc": "^2.0.0",
|
"react-sortable-hoc": "^2.0.0",
|
||||||
"react-toastify": "^8.2.0",
|
"react-toastify": "^8.2.0",
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"sass": "^1.52.1",
|
"sass": "^1.55.0",
|
||||||
"sass-loader": "^12.6.0",
|
"sass-loader": "^12.6.0",
|
||||||
"sharp": "^0.29.3",
|
"sharp": "^0.29.3",
|
||||||
"slate": "^0.72.8",
|
"slate": "^0.72.8",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export type Props = {
|
|||||||
className?: string
|
className?: string
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
forceRender?: boolean
|
forceRender?: boolean
|
||||||
permissions?: {
|
permissions?: FieldPermissions | {
|
||||||
[field: string]: FieldPermissions
|
[field: string]: FieldPermissions
|
||||||
}
|
}
|
||||||
filter?: (field: Field) => boolean
|
filter?: (field: Field) => boolean
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ const CollapsibleField: React.FC<Props> = (props) => {
|
|||||||
<RenderFields
|
<RenderFields
|
||||||
forceRender
|
forceRender
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
permissions={permissions?.fields}
|
permissions={permissions}
|
||||||
fieldTypes={fieldTypes}
|
fieldTypes={fieldTypes}
|
||||||
fieldSchema={fields.map((field) => ({
|
fieldSchema={fields.map((field) => ({
|
||||||
...field,
|
...field,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const Row: React.FC<Props> = (props) => {
|
|||||||
<RenderFields
|
<RenderFields
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
className={classes}
|
className={classes}
|
||||||
permissions={permissions?.fields}
|
permissions={permissions}
|
||||||
fieldTypes={fieldTypes}
|
fieldTypes={fieldTypes}
|
||||||
fieldSchema={fields.map((field) => ({
|
fieldSchema={fields.map((field) => ({
|
||||||
...field,
|
...field,
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const TabsField: React.FC<Props> = (props) => {
|
|||||||
key={String(activeTab.label)}
|
key={String(activeTab.label)}
|
||||||
forceRender
|
forceRender
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
permissions={permissions?.fields}
|
permissions={tabHasName(activeTab) ? permissions[activeTab.name].fields : permissions}
|
||||||
fieldTypes={fieldTypes}
|
fieldTypes={fieldTypes}
|
||||||
fieldSchema={activeTab.fields.map((field) => ({
|
fieldSchema={activeTab.fields.map((field) => ({
|
||||||
...field,
|
...field,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ $baseline-body-size : 13px !default;
|
|||||||
$baseline : math.div($baseline-px, $baseline-body-size)+rem;
|
$baseline : math.div($baseline-px, $baseline-body-size)+rem;
|
||||||
|
|
||||||
@function base($multiplier) {
|
@function base($multiplier) {
|
||||||
@return math.div($baseline-px, $baseline-body-size) * $multiplier +rem;
|
@return (math.div($baseline-px, $baseline-body-size) * $multiplier)+rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////
|
//////////////////////////////
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { PayloadRequest } from '../../express/types';
|
import { PayloadRequest } from '../../express/types';
|
||||||
import { Permissions } from '../types';
|
import { Permissions } from '../types';
|
||||||
import { adminInit as adminInitTelemetry } from '../../utilities/telemetry/events/adminInit';
|
import { adminInit as adminInitTelemetry } from '../../utilities/telemetry/events/adminInit';
|
||||||
|
import { tabHasName } from '../../fields/config/types';
|
||||||
|
|
||||||
const allOperations = ['create', 'read', 'update', 'delete'];
|
const allOperations = ['create', 'read', 'update', 'delete'];
|
||||||
|
|
||||||
@@ -66,7 +67,12 @@ async function accessOperation(args: Arguments): Promise<Permissions> {
|
|||||||
executeFieldPolicies(updatedObj, field.fields, operation);
|
executeFieldPolicies(updatedObj, field.fields, operation);
|
||||||
} else if (field.type === 'tabs') {
|
} else if (field.type === 'tabs') {
|
||||||
field.tabs.forEach((tab) => {
|
field.tabs.forEach((tab) => {
|
||||||
executeFieldPolicies(updatedObj, tab.fields, operation);
|
if (tabHasName(tab)) {
|
||||||
|
if (!updatedObj[tab.name]) updatedObj[tab.name] = { fields: {} };
|
||||||
|
executeFieldPolicies(updatedObj[tab.name].fields, tab.fields, operation);
|
||||||
|
} else {
|
||||||
|
executeFieldPolicies(updatedObj, tab.fields, operation);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -131,33 +131,50 @@ function generateFieldTypes(config: SanitizedConfig, fields: Field[]): {
|
|||||||
if (Array.isArray(field.relationTo)) {
|
if (Array.isArray(field.relationTo)) {
|
||||||
if (field.hasMany) {
|
if (field.hasMany) {
|
||||||
fieldSchema = {
|
fieldSchema = {
|
||||||
type: 'array',
|
oneOf: [
|
||||||
items: {
|
{
|
||||||
oneOf: field.relationTo.map((relation) => {
|
type: 'array',
|
||||||
const idFieldType = getCollectionIDType(config.collections, relation);
|
items: {
|
||||||
|
oneOf: field.relationTo.map((relation) => {
|
||||||
|
const idFieldType = getCollectionIDType(config.collections, relation);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
properties: {
|
properties: {
|
||||||
value: {
|
value: {
|
||||||
oneOf: [
|
|
||||||
{
|
|
||||||
type: idFieldType,
|
type: idFieldType,
|
||||||
},
|
},
|
||||||
{
|
relationTo: {
|
||||||
|
const: relation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['value', 'relationTo'],
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
oneOf: field.relationTo.map((relation) => {
|
||||||
|
return {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
properties: {
|
||||||
|
value: {
|
||||||
$ref: `#/definitions/${relation}`,
|
$ref: `#/definitions/${relation}`,
|
||||||
},
|
},
|
||||||
],
|
relationTo: {
|
||||||
},
|
const: relation,
|
||||||
relationTo: {
|
},
|
||||||
const: relation,
|
},
|
||||||
},
|
required: ['value', 'relationTo'],
|
||||||
},
|
};
|
||||||
required: ['value', 'relationTo'],
|
}),
|
||||||
};
|
},
|
||||||
}),
|
},
|
||||||
},
|
],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
fieldSchema = {
|
fieldSchema = {
|
||||||
@@ -192,17 +209,20 @@ function generateFieldTypes(config: SanitizedConfig, fields: Field[]): {
|
|||||||
|
|
||||||
if (field.hasMany) {
|
if (field.hasMany) {
|
||||||
fieldSchema = {
|
fieldSchema = {
|
||||||
type: 'array',
|
oneOf: [
|
||||||
items: {
|
{
|
||||||
oneOf: [
|
type: 'array',
|
||||||
{
|
items: {
|
||||||
type: idFieldType,
|
type: idFieldType,
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
$ref: `#/definitions/${field.relationTo}`,
|
$ref: `#/definitions/${field.relationTo}`,
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
fieldSchema = {
|
fieldSchema = {
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ export type CollectionConfig = {
|
|||||||
/**
|
/**
|
||||||
* Custom rest api endpoints
|
* Custom rest api endpoints
|
||||||
*/
|
*/
|
||||||
endpoints?: Endpoint[]
|
endpoints?: Omit<Endpoint, 'root'>[]
|
||||||
/**
|
/**
|
||||||
* Access control
|
* Access control
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export default function registerCollections(ctx: Payload): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const endpoints = buildEndpoints(collection);
|
const endpoints = buildEndpoints(collection);
|
||||||
mountEndpoints(router, endpoints);
|
mountEndpoints(ctx.express, router, endpoints);
|
||||||
|
|
||||||
ctx.router.use(`/${slug}`, router);
|
ctx.router.use(`/${slug}`, router);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const component = joi.alternatives().try(
|
|||||||
export const endpointsSchema = joi.array().items(joi.object({
|
export const endpointsSchema = joi.array().items(joi.object({
|
||||||
path: joi.string(),
|
path: joi.string(),
|
||||||
method: joi.string().valid('get', 'head', 'post', 'put', 'patch', 'delete', 'connect', 'options'),
|
method: joi.string().valid('get', 'head', 'post', 'put', 'patch', 'delete', 'connect', 'options'),
|
||||||
|
root: joi.bool(),
|
||||||
handler: joi.alternatives().try(
|
handler: joi.alternatives().try(
|
||||||
joi.array().items(joi.func()),
|
joi.array().items(joi.func()),
|
||||||
joi.func(),
|
joi.func(),
|
||||||
|
|||||||
@@ -79,16 +79,19 @@ export type AccessResult = boolean | Where;
|
|||||||
*/
|
*/
|
||||||
export type Access = (args?: any) => AccessResult | Promise<AccessResult>;
|
export type Access = (args?: any) => AccessResult | Promise<AccessResult>;
|
||||||
|
|
||||||
export interface PayloadHandler {(
|
export interface PayloadHandler {
|
||||||
|
(
|
||||||
req: PayloadRequest,
|
req: PayloadRequest,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction,
|
next: NextFunction,
|
||||||
): void }
|
): void
|
||||||
|
}
|
||||||
|
|
||||||
export type Endpoint = {
|
export type Endpoint = {
|
||||||
path: string
|
path: string
|
||||||
method: 'get' | 'head' | 'post' | 'put' | 'patch' | 'delete' | 'connect' | 'options' | string
|
method: 'get' | 'head' | 'post' | 'put' | 'patch' | 'delete' | 'connect' | 'options' | string
|
||||||
handler: PayloadHandler | PayloadHandler[]
|
handler: PayloadHandler | PayloadHandler[]
|
||||||
|
root?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AdminView = React.ComponentType<{ user: User, canAccessAdmin: 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';
|
import { Endpoint } from '../config/types';
|
||||||
|
|
||||||
function mountEndpoints(router: Router, endpoints: Endpoint[]): void {
|
function mountEndpoints(express: Express, router: Router, endpoints: Endpoint[]): void {
|
||||||
endpoints.forEach((endpoint) => {
|
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[]
|
beforeRead?: BeforeReadHook[]
|
||||||
afterRead?: AfterReadHook[]
|
afterRead?: AfterReadHook[]
|
||||||
}
|
}
|
||||||
endpoints?: Endpoint[],
|
endpoints?: Omit<Endpoint, 'root'>[],
|
||||||
access?: {
|
access?: {
|
||||||
read?: Access;
|
read?: Access;
|
||||||
readDrafts?: Access;
|
readDrafts?: Access;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function initGlobals(ctx: Payload): void {
|
|||||||
const { slug } = global;
|
const { slug } = global;
|
||||||
|
|
||||||
const endpoints = buildEndpoints(global);
|
const endpoints = buildEndpoints(global);
|
||||||
mountEndpoints(router, endpoints);
|
mountEndpoints(ctx.express, router, endpoints);
|
||||||
|
|
||||||
ctx.router.use(`/globals/${slug}`, router);
|
ctx.router.use(`/globals/${slug}`, router);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export const init = (payload: Payload, options: InitOptions): void => {
|
|||||||
initGraphQLPlayground(payload);
|
initGraphQLPlayground(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
mountEndpoints(payload.router, payload.config.endpoints);
|
mountEndpoints(options.express, payload.router, payload.config.endpoints);
|
||||||
|
|
||||||
// Bind router to API
|
// Bind router to API
|
||||||
payload.express.use(payload.config.routes.api, payload.router);
|
payload.express.use(payload.config.routes.api, payload.router);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const PublicReadabilityAccess: FieldAccess = ({ req: { user }, siblingData }) =>
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requestHeaders = {authorization: 'Bearer testBearerToken'};
|
export const requestHeaders = { authorization: 'Bearer testBearerToken' };
|
||||||
const UseRequestHeadersAccess: FieldAccess = ({ req: { headers } }) => {
|
const UseRequestHeadersAccess: FieldAccess = ({ req: { headers } }) => {
|
||||||
return !!headers && headers.authorization === requestHeaders.authorization;
|
return !!headers && headers.authorization === requestHeaders.authorization;
|
||||||
};
|
};
|
||||||
@@ -47,6 +47,50 @@ export default buildConfig({
|
|||||||
update: () => false,
|
update: () => false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'group',
|
||||||
|
name: 'group',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'restrictedGroupText',
|
||||||
|
type: 'text',
|
||||||
|
access: {
|
||||||
|
read: () => false,
|
||||||
|
update: () => false,
|
||||||
|
create: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'row',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'restrictedRowText',
|
||||||
|
type: 'text',
|
||||||
|
access: {
|
||||||
|
read: () => false,
|
||||||
|
update: () => false,
|
||||||
|
create: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'collapsible',
|
||||||
|
label: 'Access',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'restrictedCollapsibleText',
|
||||||
|
type: 'text',
|
||||||
|
access: {
|
||||||
|
read: () => false,
|
||||||
|
update: () => false,
|
||||||
|
create: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -44,7 +44,31 @@ describe('access control', () => {
|
|||||||
|
|
||||||
await page.goto(url.edit(id));
|
await page.goto(url.edit(id));
|
||||||
|
|
||||||
await expect(page.locator('input[name="restrictedField"]')).toHaveCount(0);
|
await expect(page.locator('#field-restrictedField')).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('field without read access inside a group should not show', async () => {
|
||||||
|
const { id } = await createDoc({ restrictedField: 'restricted' });
|
||||||
|
|
||||||
|
await page.goto(url.edit(id));
|
||||||
|
|
||||||
|
await expect(page.locator('#field-group__restrictedGroupText')).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('field without read access inside a collapsible should not show', async () => {
|
||||||
|
const { id } = await createDoc({ restrictedField: 'restricted' });
|
||||||
|
|
||||||
|
await page.goto(url.edit(id));
|
||||||
|
|
||||||
|
await expect(page.locator('#field-restrictedRowText')).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('field without read access inside a row should not show', async () => {
|
||||||
|
const { id } = await createDoc({ restrictedField: 'restricted' });
|
||||||
|
|
||||||
|
await page.goto(url.edit(id));
|
||||||
|
|
||||||
|
await expect(page.locator('#field-restrictedCollapsibleText')).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('restricted collection', () => {
|
describe('restricted collection', () => {
|
||||||
|
|||||||
@@ -16,14 +16,19 @@ export interface Global {
|
|||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* 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;
|
id: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
@@ -39,3 +44,55 @@ export interface User {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: 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 { devUser } from '../credentials';
|
||||||
import { buildConfig } from '../buildConfig';
|
import { buildConfig } from '../buildConfig';
|
||||||
import { openAccess } from '../helpers/configHelpers';
|
import { openAccess } from '../helpers/configHelpers';
|
||||||
import { PayloadRequest } from '../../src/express/types';
|
import { PayloadRequest } from '../../src/express/types';
|
||||||
|
import { Config } from '../../src/config/types';
|
||||||
|
|
||||||
export const collectionSlug = 'endpoints';
|
export const collectionSlug = 'endpoints';
|
||||||
export const globalSlug = 'global-endpoints';
|
export const globalSlug = 'global-endpoints';
|
||||||
|
|
||||||
export const globalEndpoint = 'global';
|
export const globalEndpoint = 'global';
|
||||||
export const applicationEndpoint = 'path';
|
export const applicationEndpoint = 'path';
|
||||||
|
export const rootEndpoint = 'root';
|
||||||
|
|
||||||
export default buildConfig({
|
const MyConfig: Config = {
|
||||||
collections: [
|
collections: [
|
||||||
{
|
{
|
||||||
slug: collectionSlug,
|
slug: collectionSlug,
|
||||||
@@ -77,6 +79,32 @@ export default buildConfig({
|
|||||||
res.json(req.body);
|
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) => {
|
onInit: async (payload) => {
|
||||||
await payload.create({
|
await payload.create({
|
||||||
@@ -87,4 +115,6 @@ export default buildConfig({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
|
||||||
|
export default buildConfig(MyConfig);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { initPayloadTest } from '../helpers/configHelpers';
|
import { initPayloadTest } from '../helpers/configHelpers';
|
||||||
import { RESTClient } from '../helpers/rest';
|
import { RESTClient } from '../helpers/rest';
|
||||||
import { applicationEndpoint, collectionSlug, globalEndpoint, globalSlug } from './config';
|
import { applicationEndpoint, collectionSlug, globalEndpoint, globalSlug, rootEndpoint } from './config';
|
||||||
|
|
||||||
require('isomorphic-fetch');
|
require('isomorphic-fetch');
|
||||||
|
|
||||||
@@ -15,21 +15,21 @@ describe('Endpoints', () => {
|
|||||||
|
|
||||||
describe('Collections', () => {
|
describe('Collections', () => {
|
||||||
it('should GET a static endpoint', async () => {
|
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(status).toBe(200);
|
||||||
expect(data.message).toStrictEqual('Hey Joey!');
|
expect(data.message).toStrictEqual('Hey Joey!');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should GET an endpoint with a parameter', async () => {
|
it('should GET an endpoint with a parameter', async () => {
|
||||||
const name = 'George';
|
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(status).toBe(200);
|
||||||
expect(data.message).toStrictEqual(`Hello ${name}!`);
|
expect(data.message).toStrictEqual(`Hello ${name}!`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should POST an endpoint with data', async () => {
|
it('should POST an endpoint with data', async () => {
|
||||||
const params = { name: 'George', age: 29 };
|
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(status).toBe(200);
|
||||||
expect(data.name).toStrictEqual(params.name);
|
expect(data.name).toStrictEqual(params.name);
|
||||||
expect(data.age).toStrictEqual(params.age);
|
expect(data.age).toStrictEqual(params.age);
|
||||||
@@ -39,7 +39,7 @@ describe('Endpoints', () => {
|
|||||||
describe('Globals', () => {
|
describe('Globals', () => {
|
||||||
it('should call custom endpoint', async () => {
|
it('should call custom endpoint', async () => {
|
||||||
const params = { globals: 'response' };
|
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(status).toBe(200);
|
||||||
expect(params).toMatchObject(data);
|
expect(params).toMatchObject(data);
|
||||||
@@ -49,7 +49,17 @@ describe('Endpoints', () => {
|
|||||||
describe('API', () => {
|
describe('API', () => {
|
||||||
it('should call custom endpoint', async () => {
|
it('should call custom endpoint', async () => {
|
||||||
const params = { app: 'response' };
|
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(status).toBe(200);
|
||||||
expect(params).toMatchObject(data);
|
expect(params).toMatchObject(data);
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ export interface ArrayField {
|
|||||||
text: string;
|
text: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
}[];
|
}[];
|
||||||
|
collapsedArray: {
|
||||||
|
text: string;
|
||||||
|
id?: string;
|
||||||
|
}[];
|
||||||
localized: {
|
localized: {
|
||||||
text: string;
|
text: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -80,6 +84,49 @@ export interface BlockField {
|
|||||||
blockType: 'tabs';
|
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: (
|
localizedBlocks: (
|
||||||
| {
|
| {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -153,6 +200,7 @@ export interface CollapsibleField {
|
|||||||
textWithinSubGroup?: string;
|
textWithinSubGroup?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
someText?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,8 +255,8 @@ export class RESTClient {
|
|||||||
return { status, doc: result };
|
return { status, doc: result };
|
||||||
}
|
}
|
||||||
|
|
||||||
async endpoint<T = any>(path: string, method = 'get', params = undefined): Promise<{status: number, data: T}> {
|
async endpoint<T = any>(path: string, method = 'get', params = undefined): Promise<{ status: number, data: T }> {
|
||||||
const response = await fetch(`${this.serverURL}/api${path}`, {
|
const response = await fetch(`${this.serverURL}${path}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11115,10 +11115,10 @@ sass-loader@^12.6.0:
|
|||||||
klona "^2.0.4"
|
klona "^2.0.4"
|
||||||
neo-async "^2.6.2"
|
neo-async "^2.6.2"
|
||||||
|
|
||||||
sass@^1.52.1:
|
sass@^1.55.0:
|
||||||
version "1.54.0"
|
version "1.55.0"
|
||||||
resolved "https://registry.npmjs.org/sass/-/sass-1.54.0.tgz#24873673265e2a4fe3d3a997f714971db2fba1f4"
|
resolved "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz#0c4d3c293cfe8f8a2e8d3b666e1cf1bff8065d1c"
|
||||||
integrity sha512-C4zp79GCXZfK0yoHZg+GxF818/aclhp9F48XBu/+bm9vXEVAYov9iU3FBVRMq3Hx3OA4jfKL+p2K9180mEh0xQ==
|
integrity sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar ">=3.0.0 <4.0.0"
|
chokidar ">=3.0.0 <4.0.0"
|
||||||
immutable "^4.0.0"
|
immutable "^4.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user