Compare commits
25 Commits
feat/resto
...
feat/live-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
207623e521 | ||
|
|
5d934ba02d | ||
|
|
f651665f2f | ||
|
|
5d3659d48a | ||
|
|
47106d5a1a | ||
|
|
afa2b942e0 | ||
|
|
20ddd0de5b | ||
|
|
64f705c3c9 | ||
|
|
b30ea8aa6b | ||
|
|
471d2113a7 | ||
|
|
8725d41164 | ||
|
|
0bd81aa25a | ||
|
|
8c09ca9be5 | ||
|
|
90d7ee3e65 | ||
|
|
58bbd8c00f | ||
|
|
003ad065c3 | ||
|
|
70715926a8 | ||
|
|
b3a6bfacf2 | ||
|
|
e1d9accb27 | ||
|
|
f2f55a84cc | ||
|
|
eba53ba60a | ||
|
|
f73d503fec | ||
|
|
6930c4e9f2 | ||
|
|
3eb681e847 | ||
|
|
cb4638cfa1 |
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,3 +1,20 @@
|
||||
## [2.9.0](https://github.com/payloadcms/payload/compare/v2.8.2...v2.9.0) (2024-01-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* forceAcceptWarning migration arg added to accept prompts ([#4874](https://github.com/payloadcms/payload/issues/4874)) ([eba53ba](https://github.com/payloadcms/payload/commit/eba53ba60afd7c5d37389377ed06a9b556058d49))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* afterLogin hook write conflicts ([#4904](https://github.com/payloadcms/payload/issues/4904)) ([3eb681e](https://github.com/payloadcms/payload/commit/3eb681e847e9c55eaaa69c22bea4f4e66c7eac36))
|
||||
* **db-postgres:** migrate down error ([#4861](https://github.com/payloadcms/payload/issues/4861)) ([dfba522](https://github.com/payloadcms/payload/commit/dfba5222f3abf3f236dc9212a28e1aec7d7214d5))
|
||||
* **db-postgres:** query unset relation ([#4862](https://github.com/payloadcms/payload/issues/4862)) ([8ce15c8](https://github.com/payloadcms/payload/commit/8ce15c8b07800397a50dcf790c263ed5b3cfad53))
|
||||
* migrate down missing filter for latest batch ([#4860](https://github.com/payloadcms/payload/issues/4860)) ([b99d24f](https://github.com/payloadcms/payload/commit/b99d24fcfa698c493ea01c41621201abe18fabe3))
|
||||
* **plugin-cloud-storage:** slow get file performance large collections ([#4927](https://github.com/payloadcms/payload/issues/4927)) ([f73d503](https://github.com/payloadcms/payload/commit/f73d503fecdfa5cefdc26ab9aad60b00563f881e))
|
||||
* remove No Options dropdown from hasMany fields ([#4899](https://github.com/payloadcms/payload/issues/4899)) ([e5a7907](https://github.com/payloadcms/payload/commit/e5a7907a72c1371447ac2f71fce213ed22246092))
|
||||
* upload input drawer does not show draft versions ([#4903](https://github.com/payloadcms/payload/issues/4903)) ([6930c4e](https://github.com/payloadcms/payload/commit/6930c4e9f2200853121391ad8f8df48ea66c40a4))
|
||||
|
||||
## [2.8.2](https://github.com/payloadcms/payload/compare/v2.8.1...v2.8.2) (2024-01-16)
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ This field uses the `monaco-react` editor syntax highlighting.
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`index`** | Build a an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
|
||||
|
||||
@@ -38,7 +38,7 @@ caption="Admin panel screenshot of a Relationship field"
|
||||
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build a an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
|
||||
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
|
||||
|
||||
@@ -6,7 +6,8 @@ desc: Hooks can be added to any fields, and optionally modify the return value o
|
||||
keywords: hooks, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
---
|
||||
|
||||
Field-level hooks offer incredible potential for encapsulating your logic. They help to isolate concerns and package up functionalities to be easily reusable across your projects.
|
||||
Field-level hooks offer incredible potential for encapsulating your logic. They help to isolate concerns and package up
|
||||
functionalities to be easily reusable across your projects.
|
||||
|
||||
**Example use cases include:**
|
||||
|
||||
@@ -46,7 +47,8 @@ const ExampleField: Field = {
|
||||
|
||||
## Arguments and return values
|
||||
|
||||
All field-level hooks are formatted to accept the same arguments, although some arguments may be `undefined` based on which field hook you are utilizing.
|
||||
All field-level hooks are formatted to accept the same arguments, although some arguments may be `undefined` based on
|
||||
which field hook you are utilizing.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
@@ -69,10 +71,10 @@ Field Hooks receive one `args` argument that contains the following properties:
|
||||
| **`operation`** | A string relating to which operation the field type is currently executing within. Useful within `beforeValidate`, `beforeChange`, and `afterChange` hooks to differentiate between `create` and `update` operations. |
|
||||
| **`originalDoc`** | The full original document in `update` operations. In the `afterChange` hook, this is the resulting document of the operation. |
|
||||
| **`previousDoc`** | The document before changes were applied, only in `afterChange` hooks. |
|
||||
| **`previousSiblingDoc`** | The sibling data from the previous document in `afterChange` hook. |
|
||||
| **`previousSiblingDoc`** | The sibling data of the document before changes being applied, only in `beforeChange` and `afterChange` hook. |
|
||||
| **`req`** | The Express `request` object. It is mocked for Local API operations. |
|
||||
| **`value`** | The value of the field. |
|
||||
| **`previousValue`** | The previous value of the field, before changes were applied, only in `afterChange` hooks. |
|
||||
| **`previousValue`** | The previous value of the field, before changes, only in `beforeChange` and `afterChange` hooks. |
|
||||
| **`context`** | Context passed to this hook. More info can be found under [Context](/docs/hooks/context) |
|
||||
| **`field`** | The field which the hook is running against. |
|
||||
| **`collection`** | The collection which the field belongs to. If the field belongs to a global, this will be null. |
|
||||
@@ -80,7 +82,8 @@ Field Hooks receive one `args` argument that contains the following properties:
|
||||
|
||||
#### Return value
|
||||
|
||||
All field hooks can optionally modify the return value of the field before the operation continues. Field Hooks may optionally return the value that should be used within the field.
|
||||
All field hooks can optionally modify the return value of the field before the operation continues. Field Hooks may
|
||||
optionally return the value that should be used within the field.
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Important</strong>
|
||||
@@ -92,11 +95,14 @@ All field hooks can optionally modify the return value of the field before the o
|
||||
|
||||
## Examples of Field Hooks
|
||||
|
||||
To better illustrate how field-level hooks can be applied, here are some specific examples. These demonstrate the flexibility and potential of field hooks in different contexts. Remember, these examples are just a starting point - the true potential of field-level hooks lies in their adaptability to a wide array of use cases.
|
||||
To better illustrate how field-level hooks can be applied, here are some specific examples. These demonstrate the
|
||||
flexibility and potential of field hooks in different contexts. Remember, these examples are just a starting point - the
|
||||
true potential of field-level hooks lies in their adaptability to a wide array of use cases.
|
||||
|
||||
### beforeValidate
|
||||
|
||||
Runs before the `update` operation. This hook allows you to pre-process or format field data before it undergoes validation.
|
||||
Runs before the `update` operation. This hook allows you to pre-process or format field data before it undergoes
|
||||
validation.
|
||||
|
||||
```ts
|
||||
import { Field } from 'payload/types'
|
||||
@@ -113,11 +119,15 @@ const usernameField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
In this example, the `beforeValidate` hook is used to process the `username` field. The hook takes the incoming value of the field and transforms it by trimming whitespace and converting it to lowercase. This ensures that the username is stored in a consistent format in the database.
|
||||
In this example, the `beforeValidate` hook is used to process the `username` field. The hook takes the incoming value of
|
||||
the field and transforms it by trimming whitespace and converting it to lowercase. This ensures that the username is
|
||||
stored in a consistent format in the database.
|
||||
|
||||
### beforeChange
|
||||
|
||||
Immediately following validation, `beforeChange` hooks will run within `create` and `update` operations. At this stage, you can be confident that the field data that will be saved to the document is valid in accordance to your field validations.
|
||||
Immediately following validation, `beforeChange` hooks will run within `create` and `update` operations. At this stage,
|
||||
you can be confident that the field data that will be saved to the document is valid in accordance to your field
|
||||
validations.
|
||||
|
||||
```ts
|
||||
import { Field } from 'payload/types'
|
||||
@@ -136,11 +146,14 @@ const emailField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
In the `emailField`, the `beforeChange` hook checks the `operation` type. If the operation is `create`, it performs additional validation or transformation on the email field value. This allows for operation-specific logic to be applied to the field.
|
||||
In the `emailField`, the `beforeChange` hook checks the `operation` type. If the operation is `create`, it performs
|
||||
additional validation or transformation on the email field value. This allows for operation-specific logic to be applied
|
||||
to the field.
|
||||
|
||||
### afterChange
|
||||
|
||||
The `afterChange` hook is executed after a field's value has been changed and saved in the database. This hook is useful for post-processing or triggering side effects based on the new value of the field.
|
||||
The `afterChange` hook is executed after a field's value has been changed and saved in the database. This hook is useful
|
||||
for post-processing or triggering side effects based on the new value of the field.
|
||||
|
||||
```ts
|
||||
import { Field } from 'payload/types'
|
||||
@@ -165,11 +178,15 @@ const membershipStatusField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
In this example, the `afterChange` hook is used with a `membershipStatusField`, which allows users to select their membership level (Standard, Premium, VIP). The hook monitors changes in the membership status. When a change occurs, it logs the update and can be used to trigger further actions, such as tracking conversion from one tier to another or notifying them about changes in their membership benefits.
|
||||
In this example, the `afterChange` hook is used with a `membershipStatusField`, which allows users to select their
|
||||
membership level (Standard, Premium, VIP). The hook monitors changes in the membership status. When a change occurs, it
|
||||
logs the update and can be used to trigger further actions, such as tracking conversion from one tier to another or
|
||||
notifying them about changes in their membership benefits.
|
||||
|
||||
### afterRead
|
||||
|
||||
The `afterRead` hook is invoked after a field value is read from the database. This is ideal for formatting or transforming the field data for output.
|
||||
The `afterRead` hook is invoked after a field value is read from the database. This is ideal for formatting or
|
||||
transforming the field data for output.
|
||||
|
||||
```ts
|
||||
import { Field } from 'payload/types'
|
||||
@@ -186,8 +203,9 @@ const dateField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
Here, the `afterRead` hook for the `dateField` is used to format the date into a more readable format using `toLocaleDateString()`. This hook modifies the way the date is presented to the user, making it more user-friendly.
|
||||
|
||||
Here, the `afterRead` hook for the `dateField` is used to format the date into a more readable format
|
||||
using `toLocaleDateString()`. This hook modifies the way the date is presented to the user, making it more
|
||||
user-friendly.
|
||||
|
||||
## TypeScript
|
||||
|
||||
|
||||
2
examples/hierarchy/.env.example
Normal file
2
examples/hierarchy/.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
DATABASE_URI=mongodb://127.0.0.1/payload-template-blank
|
||||
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||
6
examples/hierarchy/.gitignore
vendored
Normal file
6
examples/hierarchy/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
build
|
||||
dist
|
||||
/media
|
||||
node_modules
|
||||
.DS_Store
|
||||
.env
|
||||
8
examples/hierarchy/.prettierrc.js
Normal file
8
examples/hierarchy/.prettierrc.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
58
examples/hierarchy/README.md
Normal file
58
examples/hierarchy/README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Payload Hierarchy Example
|
||||
|
||||
This example demonstrates how to achieve a virtual hierarchy between documents in your [Payload](https://github.com/payloadcms/payload) application.
|
||||
|
||||
## Quick Start
|
||||
|
||||
To spin up the project locally, follow these steps:
|
||||
|
||||
1. First clone the repo
|
||||
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
|
||||
1. Next `yarn && yarn dev` (or `docker-compose up`, see [Docker](#docker))
|
||||
1. Now `open http://localhost:3000/admin` to access the admin panel
|
||||
1. Create your first admin user using the form on the page
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app.
|
||||
|
||||
## How it works
|
||||
|
||||
This example achieves parent/child relationships between your documents through the use of virtual fields. When you query a document with the `?children=true` query param, an afterRead hook is used to populate the documents within its own tree.
|
||||
|
||||
For more information on how virtual fields, see the [Official Virtual Fields Example](https://github.com/payloadcms/payload/tree/main/examples/virtual-fields).
|
||||
|
||||
### Collections
|
||||
|
||||
See the [Collections](https://payloadcms.com/docs/configuration/collections) docs for details on how to extend any of this functionality.
|
||||
|
||||
- #### Users
|
||||
|
||||
The `users` collection is a default payload users collection.
|
||||
|
||||
- #### Entities
|
||||
|
||||
The `entities` collection can define a parent as any other entity. It has a virtual field that will also populate children when it is called via the API using a query `children=true`. See [Virtual Fields](https://github.com/payloadcms/payload/tree/main/examples/virtual-fields) for more details on how virtual fields work.
|
||||
|
||||
The virtual field retrieves __all__ children which includes other entities and people.
|
||||
|
||||
- #### People
|
||||
|
||||
The `people` collection is a collection that can define an array of parent entities. It also has an allocation field. This is for demonstrating attaching data to a parent-child relationship.
|
||||
|
||||
## Development
|
||||
|
||||
To spin up this example locally, follow the [Quick Start](#quick-start).
|
||||
|
||||
## Production
|
||||
|
||||
To run Payload in production, you need to build and serve the Admin panel. To do so, follow these steps:
|
||||
|
||||
1. First invoke the `payload build` script by running `yarn build` or `npm run build` in your project root. This creates a `./build` directory with a production-ready admin bundle.
|
||||
1. Then run `yarn serve` or `npm run serve` to run Node in production and serve Payload from the `./build` directory.
|
||||
|
||||
### Deployment
|
||||
|
||||
The easiest way to deploy your project is to use [Payload Cloud](https://payloadcms.com/new/import), a one-click hosting solution to deploy production-ready instances of your Payload apps directly from your GitHub repo. You can also deploy your app manually, check out the [deployment documentation](https://payloadcms.com/docs/production/deployment) for full details.
|
||||
|
||||
## Questions
|
||||
|
||||
If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions).
|
||||
6
examples/hierarchy/nodemon.json
Normal file
6
examples/hierarchy/nodemon.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nodemon.json",
|
||||
"ext": "ts",
|
||||
"exec": "ts-node src/server.ts -- -I",
|
||||
"stdin": false
|
||||
}
|
||||
35
examples/hierarchy/package.json
Normal file
35
examples/hierarchy/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "hierarchy",
|
||||
"description": "A hierarchy example with Payload",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/server.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
|
||||
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
|
||||
"build:server": "tsc",
|
||||
"build": "yarn copyfiles && yarn build:payload && yarn build:server",
|
||||
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
|
||||
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
|
||||
"generate:graphQLSchema": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
|
||||
"payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "^1.0.0",
|
||||
"@payloadcms/db-mongodb": "^1.0.0",
|
||||
"@payloadcms/plugin-cloud": "^3.0.0",
|
||||
"@payloadcms/richtext-slate": "^1.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.9",
|
||||
"copyfiles": "^2.4.1",
|
||||
"nodemon": "^2.0.6",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
79
examples/hierarchy/src/collections/Entities.ts
Normal file
79
examples/hierarchy/src/collections/Entities.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { CollectionConfig } from 'payload/types'
|
||||
|
||||
export const Entities: CollectionConfig = {
|
||||
slug: 'entities',
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
// - This field is populated by setting the query parameter 'children=true'
|
||||
// - This is a virtual field used to track a child relationship
|
||||
// - Only relationship information is returned by this field
|
||||
// - Data beyond relationships is not stored in this field
|
||||
{
|
||||
name: 'children',
|
||||
type: 'relationship',
|
||||
relationTo: ['entities', 'people'],
|
||||
access: {
|
||||
create: () => false,
|
||||
update: () => false,
|
||||
},
|
||||
hooks: {
|
||||
afterRead: [
|
||||
async ({ data, req }) => {
|
||||
const { id } = data
|
||||
|
||||
if (!req.query.children) return
|
||||
|
||||
const people = await req.payload.find({
|
||||
req,
|
||||
collection: 'people',
|
||||
where: {
|
||||
'parents.parent': { equals: id },
|
||||
},
|
||||
limit: 0,
|
||||
depth: 0,
|
||||
pagination: false,
|
||||
})
|
||||
|
||||
const entities = await req.payload.find({
|
||||
req,
|
||||
collection: 'entities',
|
||||
where: {
|
||||
parent: { equals: id },
|
||||
},
|
||||
limit: 0,
|
||||
depth: 0,
|
||||
pagination: false,
|
||||
})
|
||||
|
||||
return [
|
||||
...entities.docs.map(entity => {
|
||||
return {
|
||||
relationTo: 'entity',
|
||||
value: entity,
|
||||
}
|
||||
}),
|
||||
...people.docs.map(person => {
|
||||
return {
|
||||
relationTo: 'people',
|
||||
value: person,
|
||||
}
|
||||
}),
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'parent',
|
||||
type: 'relationship',
|
||||
relationTo: 'entities',
|
||||
},
|
||||
],
|
||||
}
|
||||
32
examples/hierarchy/src/collections/People.ts
Normal file
32
examples/hierarchy/src/collections/People.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { CollectionConfig } from 'payload/types'
|
||||
|
||||
export const People: CollectionConfig = {
|
||||
slug: 'people',
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'parents',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'parent',
|
||||
type: 'relationship',
|
||||
relationTo: 'entities',
|
||||
},
|
||||
{
|
||||
name: 'allocation',
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
15
examples/hierarchy/src/collections/Users.ts
Normal file
15
examples/hierarchy/src/collections/Users.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { CollectionConfig } from 'payload/types'
|
||||
|
||||
const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
fields: [
|
||||
// Email added by default
|
||||
// Add more fields as needed
|
||||
],
|
||||
}
|
||||
|
||||
export default Users
|
||||
30
examples/hierarchy/src/payload.config.ts
Normal file
30
examples/hierarchy/src/payload.config.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import path from 'path'
|
||||
|
||||
import { payloadCloud } from '@payloadcms/plugin-cloud'
|
||||
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
||||
import { webpackBundler } from '@payloadcms/bundler-webpack'
|
||||
import { slateEditor } from '@payloadcms/richtext-slate'
|
||||
import { buildConfig } from 'payload/config'
|
||||
|
||||
import Users from './collections/Users'
|
||||
import { Entities } from './collections/Entities'
|
||||
import { People } from './collections/People'
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
user: Users.slug,
|
||||
bundler: webpackBundler(),
|
||||
},
|
||||
editor: slateEditor({}),
|
||||
collections: [Users, Entities, People],
|
||||
typescript: {
|
||||
outputFile: path.resolve(__dirname, 'payload-types.ts'),
|
||||
},
|
||||
graphQL: {
|
||||
schemaOutputFile: path.resolve(__dirname, 'generated-schema.graphql'),
|
||||
},
|
||||
plugins: [payloadCloud()],
|
||||
db: mongooseAdapter({
|
||||
url: process.env.DATABASE_URI,
|
||||
}),
|
||||
})
|
||||
27
examples/hierarchy/src/server.ts
Normal file
27
examples/hierarchy/src/server.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import express from 'express'
|
||||
import payload from 'payload'
|
||||
|
||||
require('dotenv').config()
|
||||
const app = express()
|
||||
|
||||
// Redirect root to Admin panel
|
||||
app.get('/', (_, res) => {
|
||||
res.redirect('/admin')
|
||||
})
|
||||
|
||||
const start = async () => {
|
||||
// Initialize Payload
|
||||
await payload.init({
|
||||
secret: process.env.PAYLOAD_SECRET,
|
||||
express: app,
|
||||
onInit: async () => {
|
||||
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`)
|
||||
},
|
||||
})
|
||||
|
||||
// Add your own express routes here
|
||||
|
||||
app.listen(3000)
|
||||
}
|
||||
|
||||
start()
|
||||
22
examples/hierarchy/tsconfig.json
Normal file
22
examples/hierarchy/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"strict": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"jsx": "react",
|
||||
"paths": {
|
||||
"payload/generated-types": ["./src/payload-types.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist", "build"],
|
||||
"ts-node": {
|
||||
"transpileOnly": true,
|
||||
"swc": true
|
||||
}
|
||||
}
|
||||
7896
examples/hierarchy/yarn.lock
Normal file
7896
examples/hierarchy/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ export const recordLastLoggedInTenant: AfterLoginHook = async ({ req, user }) =>
|
||||
},
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
req,
|
||||
})
|
||||
|
||||
if (relatedOrg.docs.length > 0) {
|
||||
@@ -20,6 +21,7 @@ export const recordLastLoggedInTenant: AfterLoginHook = async ({ req, user }) =>
|
||||
data: {
|
||||
lastLoggedInTenant: relatedOrg.docs[0].id,
|
||||
},
|
||||
req,
|
||||
})
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
|
||||
@@ -30,6 +30,7 @@ export const isSuperOrTenantAdmin = async (args: { req: PayloadRequest }): Promi
|
||||
},
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
req,
|
||||
})
|
||||
|
||||
// if this tenant does not exist, deny access
|
||||
|
||||
14
package.json
14
package.json
@@ -64,7 +64,7 @@
|
||||
"copyfiles": "2.4.1",
|
||||
"cross-env": "7.0.3",
|
||||
"dotenv": "8.6.0",
|
||||
"drizzle-orm": "0.28.5",
|
||||
"drizzle-orm": "0.29.3",
|
||||
"express": "4.18.2",
|
||||
"form-data": "3.0.1",
|
||||
"fs-extra": "10.1.0",
|
||||
@@ -94,7 +94,7 @@
|
||||
"slash": "3.0.0",
|
||||
"slate": "0.91.4",
|
||||
"tempfile": "^3.0.0",
|
||||
"ts-node": "10.9.1",
|
||||
"ts-node": "10.9.2",
|
||||
"turbo": "^1.11.1",
|
||||
"typescript": "5.2.2",
|
||||
"uuid": "^9.0.1"
|
||||
@@ -104,6 +104,16 @@
|
||||
"react-i18next": "11.18.6",
|
||||
"react-router-dom": "5.3.4"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"copyfiles": "2.4.1",
|
||||
"cross-env": "7.0.3",
|
||||
"dotenv": "8.6.0",
|
||||
"drizzle-orm": "0.29.3",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"pnpm": ">=8"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "1.3.2",
|
||||
"version": "1.4.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -11,25 +11,30 @@ import type { MongooseAdapter } from '.'
|
||||
/**
|
||||
* Drop the current database and run all migrate up functions
|
||||
*/
|
||||
export async function migrateFresh(this: MongooseAdapter): Promise<void> {
|
||||
export async function migrateFresh(
|
||||
this: MongooseAdapter,
|
||||
{ forceAcceptWarning = false }: { forceAcceptWarning?: boolean },
|
||||
): Promise<void> {
|
||||
const { payload } = this
|
||||
|
||||
const { confirm: acceptWarning } = await prompts(
|
||||
{
|
||||
name: 'confirm',
|
||||
type: 'confirm',
|
||||
initial: false,
|
||||
message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`,
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0)
|
||||
if (!forceAcceptWarning) {
|
||||
const { confirm: acceptWarning } = await prompts(
|
||||
{
|
||||
name: 'confirm',
|
||||
type: 'confirm',
|
||||
initial: false,
|
||||
message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`,
|
||||
},
|
||||
},
|
||||
)
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!acceptWarning) {
|
||||
process.exit(0)
|
||||
if (!acceptWarning) {
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
payload.logger.info({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
@@ -22,7 +22,7 @@
|
||||
"dependencies": {
|
||||
"@libsql/client": "^0.3.1",
|
||||
"console-table-printer": "2.11.2",
|
||||
"drizzle-kit": "0.20.5-608ae62",
|
||||
"drizzle-kit": "0.20.14-1f2c838",
|
||||
"drizzle-orm": "0.29.3",
|
||||
"pg": "8.11.3",
|
||||
"prompts": "2.4.2",
|
||||
|
||||
@@ -53,7 +53,7 @@ const getDefaultDrizzleSnapshot = (): DrizzleSnapshotJSON => ({
|
||||
|
||||
export const createMigration: CreateMigration = async function createMigration(
|
||||
this: PostgresAdapter,
|
||||
{ migrationName, payload },
|
||||
{ forceAcceptWarning, migrationName, payload },
|
||||
) {
|
||||
const dir = payload.db.migrationDir
|
||||
if (!fs.existsSync(dir)) {
|
||||
@@ -95,7 +95,7 @@ export const createMigration: CreateMigration = async function createMigration(
|
||||
const sqlStatementsUp = await generateMigration(drizzleJsonBefore, drizzleJsonAfter)
|
||||
const sqlStatementsDown = await generateMigration(drizzleJsonAfter, drizzleJsonBefore)
|
||||
|
||||
if (!sqlStatementsUp.length && !sqlStatementsDown.length) {
|
||||
if (!sqlStatementsUp.length && !sqlStatementsDown.length && !forceAcceptWarning) {
|
||||
const { confirm: shouldCreateBlankMigration } = await prompts(
|
||||
{
|
||||
name: 'confirm',
|
||||
|
||||
@@ -14,25 +14,30 @@ import { parseError } from './utilities/parseError'
|
||||
/**
|
||||
* Drop the current database and run all migrate up functions
|
||||
*/
|
||||
export async function migrateFresh(this: PostgresAdapter): Promise<void> {
|
||||
export async function migrateFresh(
|
||||
this: PostgresAdapter,
|
||||
{ forceAcceptWarning = false },
|
||||
): Promise<void> {
|
||||
const { payload } = this
|
||||
|
||||
const { confirm: acceptWarning } = await prompts(
|
||||
{
|
||||
name: 'confirm',
|
||||
type: 'confirm',
|
||||
initial: false,
|
||||
message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`,
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0)
|
||||
if (forceAcceptWarning === false) {
|
||||
const { confirm: acceptWarning } = await prompts(
|
||||
{
|
||||
name: 'confirm',
|
||||
type: 'confirm',
|
||||
initial: false,
|
||||
message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`,
|
||||
},
|
||||
},
|
||||
)
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!acceptWarning) {
|
||||
process.exit(0)
|
||||
if (!acceptWarning) {
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
payload.logger.info({
|
||||
|
||||
@@ -27,9 +27,9 @@ type Args = {
|
||||
adapter: PostgresAdapter
|
||||
baseColumns?: Record<string, PgColumnBuilder>
|
||||
baseExtraConfig?: Record<string, (cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder>
|
||||
buildTexts?: boolean
|
||||
buildNumbers?: boolean
|
||||
buildRelationships?: boolean
|
||||
buildTexts?: boolean
|
||||
disableNotNull: boolean
|
||||
disableUnique: boolean
|
||||
fields: Field[]
|
||||
@@ -42,8 +42,8 @@ type Args = {
|
||||
}
|
||||
|
||||
type Result = {
|
||||
hasManyTextField: 'index' | boolean
|
||||
hasManyNumberField: 'index' | boolean
|
||||
hasManyTextField: 'index' | boolean
|
||||
relationsToBuild: Map<string, string>
|
||||
}
|
||||
|
||||
@@ -51,9 +51,9 @@ export const buildTable = ({
|
||||
adapter,
|
||||
baseColumns = {},
|
||||
baseExtraConfig = {},
|
||||
buildTexts,
|
||||
buildNumbers,
|
||||
buildRelationships,
|
||||
buildTexts,
|
||||
disableNotNull,
|
||||
disableUnique = false,
|
||||
fields,
|
||||
@@ -100,16 +100,16 @@ export const buildTable = ({
|
||||
columns.id = idColTypeMap[idColType]('id').primaryKey()
|
||||
;({
|
||||
hasLocalizedField,
|
||||
hasLocalizedManyTextField,
|
||||
hasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField,
|
||||
hasManyTextField,
|
||||
hasManyNumberField,
|
||||
hasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
buildTexts,
|
||||
buildNumbers,
|
||||
buildRelationships,
|
||||
buildTexts,
|
||||
columns,
|
||||
disableNotNull,
|
||||
disableUnique,
|
||||
@@ -196,12 +196,12 @@ export const buildTable = ({
|
||||
const textsTableName = `${rootTableName}_texts`
|
||||
const columns: Record<string, PgColumnBuilder> = {
|
||||
id: serial('id').primaryKey(),
|
||||
text: varchar('text'),
|
||||
order: integer('order').notNull(),
|
||||
parent: parentIDColumnMap[idColType]('parent_id')
|
||||
.references(() => table.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
path: varchar('path').notNull(),
|
||||
text: varchar('text'),
|
||||
}
|
||||
|
||||
if (hasLocalizedManyTextField) {
|
||||
@@ -210,15 +210,15 @@ export const buildTable = ({
|
||||
|
||||
textsTable = pgTable(textsTableName, columns, (cols) => {
|
||||
const indexes: Record<string, IndexBuilder> = {
|
||||
orderParentIdx: index('order_parent_idx').on(cols.order, cols.parent),
|
||||
orderParentIdx: index(`${textsTableName}_order_parent_idx`).on(cols.order, cols.parent),
|
||||
}
|
||||
|
||||
if (hasManyTextField === 'index') {
|
||||
indexes.text_idx = index('text_idx').on(cols.text)
|
||||
indexes.text_idx = index(`${textsTableName}_text_idx`).on(cols.text)
|
||||
}
|
||||
|
||||
if (hasLocalizedManyTextField) {
|
||||
indexes.localeParent = index('locale_parent').on(cols.locale, cols.parent)
|
||||
indexes.localeParent = index(`${textsTableName}_locale_parent`).on(cols.locale, cols.parent)
|
||||
}
|
||||
|
||||
return indexes
|
||||
@@ -254,15 +254,18 @@ export const buildTable = ({
|
||||
|
||||
numbersTable = pgTable(numbersTableName, columns, (cols) => {
|
||||
const indexes: Record<string, IndexBuilder> = {
|
||||
orderParentIdx: index('order_parent_idx').on(cols.order, cols.parent),
|
||||
orderParentIdx: index(`${numbersTableName}_order_parent_idx`).on(cols.order, cols.parent),
|
||||
}
|
||||
|
||||
if (hasManyNumberField === 'index') {
|
||||
indexes.numberIdx = index('number_idx').on(cols.number)
|
||||
indexes.numberIdx = index(`${numbersTableName}_number_idx`).on(cols.number)
|
||||
}
|
||||
|
||||
if (hasLocalizedManyNumberField) {
|
||||
indexes.localeParent = index('locale_parent').on(cols.locale, cols.parent)
|
||||
indexes.localeParent = index(`${numbersTableName}_locale_parent`).on(
|
||||
cols.locale,
|
||||
cols.parent,
|
||||
)
|
||||
}
|
||||
|
||||
return indexes
|
||||
@@ -313,13 +316,13 @@ export const buildTable = ({
|
||||
|
||||
relationshipsTable = pgTable(relationshipsTableName, relationshipColumns, (cols) => {
|
||||
const result: Record<string, unknown> = {
|
||||
order: index('order_idx').on(cols.order),
|
||||
parentIdx: index('parent_idx').on(cols.parent),
|
||||
pathIdx: index('path_idx').on(cols.path),
|
||||
order: index(`${relationshipsTableName}_order_idx`).on(cols.order),
|
||||
parentIdx: index(`${relationshipsTableName}_parent_idx`).on(cols.parent),
|
||||
pathIdx: index(`${relationshipsTableName}_path_idx`).on(cols.path),
|
||||
}
|
||||
|
||||
if (hasLocalizedRelationshipField) {
|
||||
result.localeIdx = index('locale_idx').on(cols.locale)
|
||||
result.localeIdx = index(`${relationshipsTableName}_locale_idx`).on(cols.locale)
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -381,5 +384,5 @@ export const buildTable = ({
|
||||
|
||||
adapter.relations[`relations_${tableName}`] = tableRelations
|
||||
|
||||
return { hasManyTextField, hasManyNumberField, relationsToBuild }
|
||||
return { hasManyNumberField, hasManyTextField, relationsToBuild }
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@ import type { GenericColumn } from '../types'
|
||||
type CreateIndexArgs = {
|
||||
columnName: string
|
||||
name: string | string[]
|
||||
tableName: string
|
||||
unique?: boolean
|
||||
}
|
||||
|
||||
export const createIndex = ({ name, columnName, unique }: CreateIndexArgs) => {
|
||||
export const createIndex = ({ name, columnName, tableName, unique }: CreateIndexArgs) => {
|
||||
return (table: { [x: string]: GenericColumn }) => {
|
||||
let columns
|
||||
if (Array.isArray(name)) {
|
||||
@@ -20,7 +21,8 @@ export const createIndex = ({ name, columnName, unique }: CreateIndexArgs) => {
|
||||
} else {
|
||||
columns = [table[name]]
|
||||
}
|
||||
if (unique) return uniqueIndex(`${columnName}_idx`).on(columns[0], ...columns.slice(1))
|
||||
return index(`${columnName}_idx`).on(columns[0], ...columns.slice(1))
|
||||
if (unique)
|
||||
return uniqueIndex(`${tableName}_${columnName}_idx`).on(columns[0], ...columns.slice(1))
|
||||
return index(`${tableName}_${columnName}_idx`).on(columns[0], ...columns.slice(1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdent
|
||||
|
||||
type Args = {
|
||||
adapter: PostgresAdapter
|
||||
buildTexts: boolean
|
||||
buildNumbers: boolean
|
||||
buildRelationships: boolean
|
||||
buildTexts: boolean
|
||||
columnPrefix?: string
|
||||
columns: Record<string, PgColumnBuilder>
|
||||
disableNotNull: boolean
|
||||
@@ -56,18 +56,18 @@ type Args = {
|
||||
|
||||
type Result = {
|
||||
hasLocalizedField: boolean
|
||||
hasLocalizedManyTextField: boolean
|
||||
hasLocalizedManyNumberField: boolean
|
||||
hasLocalizedManyTextField: boolean
|
||||
hasLocalizedRelationshipField: boolean
|
||||
hasManyTextField: 'index' | boolean
|
||||
hasManyNumberField: 'index' | boolean
|
||||
hasManyTextField: 'index' | boolean
|
||||
}
|
||||
|
||||
export const traverseFields = ({
|
||||
adapter,
|
||||
buildTexts,
|
||||
buildNumbers,
|
||||
buildRelationships,
|
||||
buildTexts,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull,
|
||||
@@ -122,7 +122,7 @@ export const traverseFields = ({
|
||||
if (
|
||||
(field.unique || field.index) &&
|
||||
!['array', 'blocks', 'group', 'point', 'relationship', 'upload'].includes(field.type) &&
|
||||
!(field.type === 'number' && field.hasMany === true)
|
||||
!('hasMany' in field && field.hasMany === true)
|
||||
) {
|
||||
const unique = disableUnique !== true && field.unique
|
||||
if (unique) {
|
||||
@@ -132,9 +132,10 @@ export const traverseFields = ({
|
||||
}
|
||||
adapter.fieldConstraints[rootTableName][`${columnName}_idx`] = constraintValue
|
||||
}
|
||||
targetIndexes[`${field.name}Idx`] = createIndex({
|
||||
targetIndexes[`${newTableName}_${field.name}Idx`] = createIndex({
|
||||
name: fieldName,
|
||||
columnName,
|
||||
tableName: newTableName,
|
||||
unique,
|
||||
})
|
||||
}
|
||||
@@ -241,17 +242,18 @@ export const traverseFields = ({
|
||||
string,
|
||||
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
|
||||
> = {
|
||||
orderIdx: (cols) => index('order_idx').on(cols.order),
|
||||
parentIdx: (cols) => index('parent_idx').on(cols.parent),
|
||||
orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order),
|
||||
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
|
||||
}
|
||||
|
||||
if (field.localized) {
|
||||
baseColumns.locale = adapter.enums.enum__locales('locale').notNull()
|
||||
baseExtraConfig.localeIdx = (cols) => index('locale_idx').on(cols.locale)
|
||||
baseExtraConfig.localeIdx = (cols) =>
|
||||
index(`${selectTableName}_locale_idx`).on(cols.locale)
|
||||
}
|
||||
|
||||
if (field.index) {
|
||||
baseExtraConfig.value = (cols) => index('value_idx').on(cols.value)
|
||||
baseExtraConfig.value = (cols) => index(`${selectTableName}_value_idx`).on(cols.value)
|
||||
}
|
||||
|
||||
buildTable({
|
||||
@@ -304,18 +306,19 @@ export const traverseFields = ({
|
||||
string,
|
||||
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
|
||||
> = {
|
||||
_orderIdx: (cols) => index('_order_idx').on(cols._order),
|
||||
_parentIDIdx: (cols) => index('_parent_id_idx').on(cols._parentID),
|
||||
_orderIdx: (cols) => index(`${arrayTableName}_order_idx`).on(cols._order),
|
||||
_parentIDIdx: (cols) => index(`${arrayTableName}_parent_id_idx`).on(cols._parentID),
|
||||
}
|
||||
|
||||
if (field.localized && adapter.payload.config.localization) {
|
||||
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
||||
baseExtraConfig._localeIdx = (cols) => index('_locale_idx').on(cols._locale)
|
||||
baseExtraConfig._localeIdx = (cols) =>
|
||||
index(`${arrayTableName}_locale_idx`).on(cols._locale)
|
||||
}
|
||||
|
||||
const {
|
||||
hasManyTextField: subHasManyTextField,
|
||||
hasManyNumberField: subHasManyNumberField,
|
||||
hasManyTextField: subHasManyTextField,
|
||||
relationsToBuild: subRelationsToBuild,
|
||||
} = buildTable({
|
||||
adapter,
|
||||
@@ -384,19 +387,20 @@ export const traverseFields = ({
|
||||
string,
|
||||
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
|
||||
> = {
|
||||
_orderIdx: (cols) => index('order_idx').on(cols._order),
|
||||
_parentIDIdx: (cols) => index('parent_id_idx').on(cols._parentID),
|
||||
_pathIdx: (cols) => index('path_idx').on(cols._path),
|
||||
_orderIdx: (cols) => index(`${blockTableName}_order_idx`).on(cols._order),
|
||||
_parentIDIdx: (cols) => index(`${blockTableName}_parent_id_idx`).on(cols._parentID),
|
||||
_pathIdx: (cols) => index(`${blockTableName}_path_idx`).on(cols._path),
|
||||
}
|
||||
|
||||
if (field.localized && adapter.payload.config.localization) {
|
||||
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
||||
baseExtraConfig._localeIdx = (cols) => index('locale_idx').on(cols._locale)
|
||||
baseExtraConfig._localeIdx = (cols) =>
|
||||
index(`${blockTableName}_locale_idx`).on(cols._locale)
|
||||
}
|
||||
|
||||
const {
|
||||
hasManyTextField: subHasManyTextField,
|
||||
hasManyNumberField: subHasManyNumberField,
|
||||
hasManyTextField: subHasManyTextField,
|
||||
relationsToBuild: subRelationsToBuild,
|
||||
} = buildTable({
|
||||
adapter,
|
||||
@@ -465,16 +469,16 @@ export const traverseFields = ({
|
||||
if (!('name' in field)) {
|
||||
const {
|
||||
hasLocalizedField: groupHasLocalizedField,
|
||||
hasLocalizedManyTextField: groupHasLocalizedManyTextField,
|
||||
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: groupHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: groupHasLocalizedRelationshipField,
|
||||
hasManyTextField: groupHasManyTextField,
|
||||
hasManyNumberField: groupHasManyNumberField,
|
||||
hasManyTextField: groupHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
buildTexts,
|
||||
buildNumbers,
|
||||
buildRelationships,
|
||||
buildTexts,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull,
|
||||
@@ -507,16 +511,16 @@ export const traverseFields = ({
|
||||
|
||||
const {
|
||||
hasLocalizedField: groupHasLocalizedField,
|
||||
hasLocalizedManyTextField: groupHasLocalizedManyTextField,
|
||||
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: groupHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: groupHasLocalizedRelationshipField,
|
||||
hasManyTextField: groupHasManyTextField,
|
||||
hasManyNumberField: groupHasManyNumberField,
|
||||
hasManyTextField: groupHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
buildTexts,
|
||||
buildNumbers,
|
||||
buildRelationships,
|
||||
buildTexts,
|
||||
columnPrefix: `${columnName}_`,
|
||||
columns,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
@@ -550,16 +554,16 @@ export const traverseFields = ({
|
||||
|
||||
const {
|
||||
hasLocalizedField: tabHasLocalizedField,
|
||||
hasLocalizedManyTextField: tabHasLocalizedManyTextField,
|
||||
hasLocalizedManyNumberField: tabHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: tabHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: tabHasLocalizedRelationshipField,
|
||||
hasManyTextField: tabHasManyTextField,
|
||||
hasManyNumberField: tabHasManyNumberField,
|
||||
hasManyTextField: tabHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
buildTexts,
|
||||
buildNumbers,
|
||||
buildRelationships,
|
||||
buildTexts,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
@@ -593,16 +597,16 @@ export const traverseFields = ({
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
const {
|
||||
hasLocalizedField: rowHasLocalizedField,
|
||||
hasLocalizedManyTextField: rowHasLocalizedManyTextField,
|
||||
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: rowHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: rowHasLocalizedRelationshipField,
|
||||
hasManyTextField: rowHasManyTextField,
|
||||
hasManyNumberField: rowHasManyNumberField,
|
||||
hasManyTextField: rowHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
buildTexts,
|
||||
buildNumbers,
|
||||
buildRelationships,
|
||||
buildTexts,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
@@ -663,10 +667,10 @@ export const traverseFields = ({
|
||||
|
||||
return {
|
||||
hasLocalizedField,
|
||||
hasLocalizedManyTextField,
|
||||
hasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField,
|
||||
hasManyTextField,
|
||||
hasManyNumberField,
|
||||
hasManyTextField,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,15 @@ import type { NodePgDatabase, NodePgQueryResultHKT } from 'drizzle-orm/node-post
|
||||
import type { PgColumn, PgEnum, PgTableWithColumns, PgTransaction } from 'drizzle-orm/pg-core'
|
||||
import type { Payload } from 'payload'
|
||||
import type { BaseDatabaseAdapter } from 'payload/database'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
import type { Pool, PoolConfig } from 'pg'
|
||||
|
||||
export type DrizzleDB = NodePgDatabase<Record<string, unknown>>
|
||||
|
||||
export type Args = {
|
||||
logger?: DrizzleConfig['logger']
|
||||
migrationDir?: string
|
||||
pool: PoolConfig
|
||||
logger?: DrizzleConfig['logger']
|
||||
push?: boolean
|
||||
}
|
||||
|
||||
@@ -49,8 +50,13 @@ export type DrizzleTransaction = PgTransaction<
|
||||
|
||||
export type PostgresAdapter = BaseDatabaseAdapter & {
|
||||
drizzle: DrizzleDB
|
||||
logger: DrizzleConfig['logger']
|
||||
enums: Record<string, GenericEnum>
|
||||
/**
|
||||
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name
|
||||
* Used for returning properly formed errors from unique fields
|
||||
*/
|
||||
fieldConstraints: Record<string, Record<string, string>>
|
||||
logger: DrizzleConfig['logger']
|
||||
pool: Pool
|
||||
poolOptions: Args['pool']
|
||||
push: boolean
|
||||
@@ -64,17 +70,12 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
|
||||
}
|
||||
}
|
||||
tables: Record<string, GenericTable>
|
||||
/**
|
||||
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name
|
||||
* Used for returning properly formed errors from unique fields
|
||||
*/
|
||||
fieldConstraints: Record<string, Record<string, string>>
|
||||
}
|
||||
|
||||
export type PostgresAdapterResult = (args: { payload: Payload }) => PostgresAdapter
|
||||
|
||||
export type MigrateUpArgs = { payload: Payload }
|
||||
export type MigrateDownArgs = { payload: Payload }
|
||||
export type MigrateUpArgs = { payload: Payload; req?: Partial<PayloadRequest> }
|
||||
export type MigrateDownArgs = { payload: Payload; req?: Partial<PayloadRequest> }
|
||||
|
||||
declare module 'payload' {
|
||||
export interface DatabaseAdapter
|
||||
@@ -82,6 +83,7 @@ declare module 'payload' {
|
||||
BaseDatabaseAdapter {
|
||||
drizzle: DrizzleDB
|
||||
enums: Record<string, GenericEnum>
|
||||
fieldConstraints: Record<string, Record<string, string>>
|
||||
pool: Pool
|
||||
push: boolean
|
||||
relations: Record<string, GenericRelation>
|
||||
@@ -94,6 +96,5 @@ declare module 'payload' {
|
||||
}
|
||||
}
|
||||
tables: Record<string, GenericTable>
|
||||
fieldConstraints: Record<string, Record<string, string>>
|
||||
}
|
||||
}
|
||||
|
||||
10
packages/live-preview-vue/.eslintignore
Normal file
10
packages/live-preview-vue/.eslintignore
Normal file
@@ -0,0 +1,10 @@
|
||||
.tmp
|
||||
**/.git
|
||||
**/.hg
|
||||
**/.pnp.*
|
||||
**/.svn
|
||||
**/.yarn/**
|
||||
**/build
|
||||
**/dist/**
|
||||
**/node_modules
|
||||
**/temp
|
||||
37
packages/live-preview-vue/.eslintrc.js
Normal file
37
packages/live-preview-vue/.eslintrc.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/** @type {import('prettier').Config} */
|
||||
module.exports = {
|
||||
extends: ['@payloadcms'],
|
||||
overrides: [
|
||||
{
|
||||
extends: ['plugin:@typescript-eslint/disable-type-checked'],
|
||||
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
|
||||
},
|
||||
{
|
||||
files: ['package.json', 'tsconfig.json'],
|
||||
rules: {
|
||||
'perfectionist/sort-array-includes': 'off',
|
||||
'perfectionist/sort-astro-attributes': 'off',
|
||||
'perfectionist/sort-classes': 'off',
|
||||
'perfectionist/sort-enums': 'off',
|
||||
'perfectionist/sort-exports': 'off',
|
||||
'perfectionist/sort-imports': 'off',
|
||||
'perfectionist/sort-interfaces': 'off',
|
||||
'perfectionist/sort-jsx-props': 'off',
|
||||
'perfectionist/sort-keys': 'off',
|
||||
'perfectionist/sort-maps': 'off',
|
||||
'perfectionist/sort-named-exports': 'off',
|
||||
'perfectionist/sort-named-imports': 'off',
|
||||
'perfectionist/sort-object-types': 'off',
|
||||
'perfectionist/sort-objects': 'off',
|
||||
'perfectionist/sort-svelte-attributes': 'off',
|
||||
'perfectionist/sort-union-types': 'off',
|
||||
'perfectionist/sort-vue-attributes': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
root: true,
|
||||
}
|
||||
10
packages/live-preview-vue/.prettierignore
Normal file
10
packages/live-preview-vue/.prettierignore
Normal file
@@ -0,0 +1,10 @@
|
||||
.tmp
|
||||
**/.git
|
||||
**/.hg
|
||||
**/.pnp.*
|
||||
**/.svn
|
||||
**/.yarn/**
|
||||
**/build
|
||||
**/dist/**
|
||||
**/node_modules
|
||||
**/temp
|
||||
15
packages/live-preview-vue/.swcrc
Normal file
15
packages/live-preview-vue/.swcrc
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"sourceMaps": "inline",
|
||||
"jsc": {
|
||||
"target": "esnext",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true,
|
||||
"dts": true
|
||||
}
|
||||
},
|
||||
"module": {
|
||||
"type": "commonjs"
|
||||
}
|
||||
}
|
||||
42
packages/live-preview-vue/package.json
Normal file
42
packages/live-preview-vue/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "0.2.0",
|
||||
"description": "The official live preview Vue SDK for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"author": "Payload CMS, Inc.",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
||||
"prepublishOnly": "pnpm clean && pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/live-preview": "workspace:^0.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"vue": "^3.0.0",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"default": "./src/index.ts",
|
||||
"types": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": null,
|
||||
"main": "./dist/index.js",
|
||||
"registry": "https://registry.npmjs.org/",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
51
packages/live-preview-vue/src/index.ts
Normal file
51
packages/live-preview-vue/src/index.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// TODO: replace React with Vue
|
||||
|
||||
import { ready, subscribe, unsubscribe } from '@payloadcms/live-preview'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
export const useLivePreview = <T extends any>(props: {
|
||||
apiRoute?: string
|
||||
depth?: number
|
||||
initialData: T
|
||||
serverURL: string
|
||||
}): {
|
||||
data: T
|
||||
isLoading: boolean
|
||||
} => {
|
||||
const { apiRoute, depth, initialData, serverURL } = props
|
||||
const [data, setData] = useState<T>(initialData)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
const hasSentReadyMessage = useRef<boolean>(false)
|
||||
|
||||
const onChange = useCallback((mergedData) => {
|
||||
setData(mergedData)
|
||||
setIsLoading(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = subscribe({
|
||||
apiRoute,
|
||||
callback: onChange,
|
||||
depth,
|
||||
initialData,
|
||||
serverURL,
|
||||
})
|
||||
|
||||
if (!hasSentReadyMessage.current) {
|
||||
hasSentReadyMessage.current = true
|
||||
|
||||
ready({
|
||||
serverURL,
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
unsubscribe(subscription)
|
||||
}
|
||||
}, [serverURL, onChange, depth, initialData, apiRoute])
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
25
packages/live-preview-vue/tsconfig.json
Normal file
25
packages/live-preview-vue/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true, // Make sure typescript knows that this module depends on their references
|
||||
"noEmit": false /* Do not emit outputs. */,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||
"rootDir": "./src" /* Specify the root folder within your source files. */,
|
||||
"jsx": "react"
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
"build",
|
||||
"tests",
|
||||
"test",
|
||||
"node_modules",
|
||||
".eslintrc.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.spec.tsx"
|
||||
],
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
|
||||
"references": [{ "path": "../payload" }] // db-mongodb depends on payload
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "2.8.2",
|
||||
"version": "2.9.0",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
@@ -59,7 +59,7 @@
|
||||
"@faceless-ui/scroll-info": "1.3.0",
|
||||
"@faceless-ui/window-info": "2.1.1",
|
||||
"@monaco-editor/react": "4.5.1",
|
||||
"@swc/core": "1.3.76",
|
||||
"@swc/core": "1.3.107",
|
||||
"@swc/register": "0.1.10",
|
||||
"body-parser": "1.20.2",
|
||||
"body-scroll-lock": "4.0.0-beta.0",
|
||||
|
||||
@@ -33,7 +33,7 @@ export const DocumentControls: React.FC<{
|
||||
id?: string
|
||||
isAccountView?: boolean
|
||||
isEditing?: boolean
|
||||
permissions?: CollectionPermission | GlobalPermission | null
|
||||
permissions?: CollectionPermission | GlobalPermission
|
||||
}> = (props) => {
|
||||
const {
|
||||
id,
|
||||
|
||||
@@ -20,7 +20,7 @@ export const getCustomViews = (args: {
|
||||
? collection?.admin?.components?.views?.Edit
|
||||
: undefined
|
||||
|
||||
const defaultViewKeys = Object.keys(defaultCollectionViews)
|
||||
const defaultViewKeys = Object.keys(defaultCollectionViews())
|
||||
|
||||
customViews = Object.entries(collectionViewsConfig || {}).reduce((prev, [key, view]) => {
|
||||
if (defaultViewKeys.includes(key)) {
|
||||
@@ -38,7 +38,7 @@ export const getCustomViews = (args: {
|
||||
? global?.admin?.components?.views?.Edit
|
||||
: undefined
|
||||
|
||||
const defaultViewKeys = Object.keys(defaultGlobalViews)
|
||||
const defaultViewKeys = Object.keys(defaultGlobalViews())
|
||||
|
||||
customViews = Object.entries(globalViewsConfig || {}).reduce((prev, [key, view]) => {
|
||||
if (defaultViewKeys.includes(key)) {
|
||||
|
||||
@@ -133,9 +133,10 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
const moreThanOneAvailableCollection = enabledCollectionConfigs.length > 1
|
||||
|
||||
useEffect(() => {
|
||||
const { slug, admin: { listSearchableFields } = {} } = selectedCollectionConfig
|
||||
const { slug, admin: { listSearchableFields } = {}, versions } = selectedCollectionConfig
|
||||
const params: {
|
||||
cacheBust?: number
|
||||
draft?: string
|
||||
limit?: number
|
||||
page?: number
|
||||
search?: string
|
||||
@@ -172,6 +173,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
if (sort) params.sort = sort
|
||||
if (cacheBust) params.cacheBust = cacheBust
|
||||
if (copyOfWhere) params.where = copyOfWhere
|
||||
if (versions?.drafts) params.draft = 'true'
|
||||
|
||||
setParams(params)
|
||||
}, [
|
||||
|
||||
@@ -12,42 +12,83 @@ import { fieldAffectsData, fieldHasSubFields, tabHasName } from '../../../../../
|
||||
import getValueWithDefault from '../../../../../fields/getDefaultValue'
|
||||
import { iterateFields } from './iterateFields'
|
||||
|
||||
type Args = {
|
||||
export type AddFieldStatePromiseArgs = {
|
||||
/**
|
||||
* if all parents are localized, then the field is localized
|
||||
*/
|
||||
anyParentLocalized?: boolean
|
||||
config: SanitizedConfig
|
||||
data: Data
|
||||
field: NonPresentationalField
|
||||
/**
|
||||
* You can use this to filter down to only `localized` fields that require transalation (type: text, textarea, etc.). Another plugin might want to look for only `point` type fields to do some GIS function. With the filter function you can go in like a surgeon.
|
||||
*/
|
||||
filter?: (args: AddFieldStatePromiseArgs) => boolean
|
||||
/**
|
||||
* Force the value of fields like arrays or blocks to be the full value instead of the length @default false
|
||||
*/
|
||||
forceFullValue?: boolean
|
||||
fullData: Data
|
||||
id: number | string
|
||||
/**
|
||||
* Whether the field schema should be included in the state
|
||||
*/
|
||||
includeSchema?: boolean
|
||||
locale: string
|
||||
/**
|
||||
* Whether to omit parent fields in the state. @default false
|
||||
*/
|
||||
omitParents?: boolean
|
||||
operation: 'create' | 'update'
|
||||
passesCondition: boolean
|
||||
path: string
|
||||
preferences: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
/**
|
||||
* Whether to skip checking the field's condition. @default false
|
||||
*/
|
||||
skipConditionChecks?: boolean
|
||||
/**
|
||||
* Whether to skip validating the field. @default false
|
||||
*/
|
||||
skipValidation?: boolean
|
||||
state: Fields
|
||||
t: TFunction
|
||||
user: User
|
||||
}
|
||||
|
||||
export const addFieldStatePromise = async ({
|
||||
id,
|
||||
config,
|
||||
data,
|
||||
field,
|
||||
fullData,
|
||||
locale,
|
||||
operation,
|
||||
passesCondition,
|
||||
path,
|
||||
preferences,
|
||||
state,
|
||||
t,
|
||||
user,
|
||||
}: Args): Promise<void> => {
|
||||
/**
|
||||
* Flattens the fields schema and fields data.
|
||||
* The output is the field path (e.g. array.0.name) mapped to a FormField object.
|
||||
*/
|
||||
export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Promise<void> => {
|
||||
const {
|
||||
id,
|
||||
anyParentLocalized = false,
|
||||
config,
|
||||
data,
|
||||
field,
|
||||
filter,
|
||||
forceFullValue = false,
|
||||
fullData,
|
||||
includeSchema = false,
|
||||
locale,
|
||||
omitParents = false,
|
||||
operation,
|
||||
passesCondition,
|
||||
path,
|
||||
preferences,
|
||||
skipConditionChecks = false,
|
||||
skipValidation = false,
|
||||
state,
|
||||
t,
|
||||
user,
|
||||
} = args
|
||||
if (fieldAffectsData(field)) {
|
||||
const fieldState: FormField = {
|
||||
condition: field.admin?.condition,
|
||||
fieldSchema: includeSchema ? field : undefined,
|
||||
initialValue: undefined,
|
||||
passesCondition,
|
||||
valid: true,
|
||||
@@ -66,9 +107,9 @@ export const addFieldStatePromise = async ({
|
||||
data[field.name] = valueWithDefault
|
||||
}
|
||||
|
||||
let validationResult: boolean | string = true
|
||||
let validationResult: string | true = true
|
||||
|
||||
if (typeof fieldState.validate === 'function') {
|
||||
if (typeof fieldState.validate === 'function' && !skipValidation) {
|
||||
validationResult = await fieldState.validate(data?.[field.name], {
|
||||
...field,
|
||||
id,
|
||||
@@ -96,24 +137,36 @@ export const addFieldStatePromise = async ({
|
||||
const rowPath = `${path}${field.name}.${i}.`
|
||||
row.id = row?.id || new ObjectID().toHexString()
|
||||
|
||||
state[`${rowPath}id`] = {
|
||||
initialValue: row.id,
|
||||
valid: true,
|
||||
value: row.id,
|
||||
if (!omitParents && (!filter || filter(args))) {
|
||||
state[`${rowPath}id`] = {
|
||||
fieldSchema: includeSchema
|
||||
? field.fields.find((field) => 'name' in field && field.name === 'id')
|
||||
: undefined,
|
||||
initialValue: row.id,
|
||||
valid: true,
|
||||
value: row.id,
|
||||
}
|
||||
}
|
||||
|
||||
acc.promises.push(
|
||||
iterateFields({
|
||||
id,
|
||||
anyParentLocalized: field.localized || anyParentLocalized,
|
||||
config,
|
||||
data: row,
|
||||
fields: field.fields,
|
||||
filter,
|
||||
forceFullValue,
|
||||
fullData,
|
||||
includeSchema,
|
||||
locale,
|
||||
omitParents,
|
||||
operation,
|
||||
parentPassesCondition: passesCondition,
|
||||
path: rowPath,
|
||||
preferences,
|
||||
skipConditionChecks,
|
||||
skipValidation,
|
||||
state,
|
||||
t,
|
||||
user,
|
||||
@@ -146,8 +199,8 @@ export const addFieldStatePromise = async ({
|
||||
fieldState.value = null
|
||||
fieldState.initialValue = null
|
||||
} else {
|
||||
fieldState.value = arrayValue.length
|
||||
fieldState.initialValue = arrayValue.length
|
||||
fieldState.value = forceFullValue ? arrayValue : arrayValue.length
|
||||
fieldState.initialValue = forceFullValue ? arrayValue : arrayValue.length
|
||||
|
||||
if (arrayValue.length > 0) {
|
||||
fieldState.disableFormData = true
|
||||
@@ -157,7 +210,9 @@ export const addFieldStatePromise = async ({
|
||||
fieldState.rows = rowMetadata
|
||||
|
||||
// Add field to state
|
||||
state[`${path}${field.name}`] = fieldState
|
||||
if (!omitParents && (!filter || filter(args))) {
|
||||
state[`${path}${field.name}`] = fieldState
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
@@ -173,36 +228,60 @@ export const addFieldStatePromise = async ({
|
||||
if (block) {
|
||||
row.id = row?.id || new ObjectID().toHexString()
|
||||
|
||||
state[`${rowPath}id`] = {
|
||||
initialValue: row.id,
|
||||
valid: true,
|
||||
value: row.id,
|
||||
}
|
||||
if (!omitParents && (!filter || filter(args))) {
|
||||
state[`${rowPath}id`] = {
|
||||
fieldSchema: includeSchema
|
||||
? block.fields.find(
|
||||
(blockField) => 'name' in blockField && blockField.name === 'id',
|
||||
)
|
||||
: undefined,
|
||||
initialValue: row.id,
|
||||
valid: true,
|
||||
value: row.id,
|
||||
}
|
||||
|
||||
state[`${rowPath}blockType`] = {
|
||||
initialValue: row.blockType,
|
||||
valid: true,
|
||||
value: row.blockType,
|
||||
}
|
||||
state[`${rowPath}blockType`] = {
|
||||
fieldSchema: includeSchema
|
||||
? block.fields.find(
|
||||
(blockField) => 'name' in blockField && blockField.name === 'blockType',
|
||||
)
|
||||
: undefined,
|
||||
initialValue: row.blockType,
|
||||
valid: true,
|
||||
value: row.blockType,
|
||||
}
|
||||
|
||||
state[`${rowPath}blockName`] = {
|
||||
initialValue: row.blockName,
|
||||
valid: true,
|
||||
value: row.blockName,
|
||||
state[`${rowPath}blockName`] = {
|
||||
fieldSchema: includeSchema
|
||||
? block.fields.find(
|
||||
(blockField) => 'name' in blockField && blockField.name === 'blockName',
|
||||
)
|
||||
: undefined,
|
||||
initialValue: row.blockName,
|
||||
valid: true,
|
||||
value: row.blockName,
|
||||
}
|
||||
}
|
||||
|
||||
acc.promises.push(
|
||||
iterateFields({
|
||||
id,
|
||||
anyParentLocalized: field.localized || anyParentLocalized,
|
||||
config,
|
||||
data: row,
|
||||
fields: block.fields,
|
||||
filter,
|
||||
forceFullValue,
|
||||
fullData,
|
||||
includeSchema,
|
||||
locale,
|
||||
omitParents,
|
||||
operation,
|
||||
parentPassesCondition: passesCondition,
|
||||
path: rowPath,
|
||||
preferences,
|
||||
skipConditionChecks,
|
||||
skipValidation,
|
||||
state,
|
||||
t,
|
||||
user,
|
||||
@@ -237,8 +316,8 @@ export const addFieldStatePromise = async ({
|
||||
fieldState.value = null
|
||||
fieldState.initialValue = null
|
||||
} else {
|
||||
fieldState.value = blocksValue.length
|
||||
fieldState.initialValue = blocksValue.length
|
||||
fieldState.value = forceFullValue ? blocksValue : blocksValue.length
|
||||
fieldState.initialValue = forceFullValue ? blocksValue : blocksValue.length
|
||||
|
||||
if (blocksValue.length > 0) {
|
||||
fieldState.disableFormData = true
|
||||
@@ -248,7 +327,9 @@ export const addFieldStatePromise = async ({
|
||||
fieldState.rows = rowMetadata
|
||||
|
||||
// Add field to state
|
||||
state[`${path}${field.name}`] = fieldState
|
||||
if (!omitParents && (!filter || filter(args))) {
|
||||
state[`${path}${field.name}`] = fieldState
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
@@ -256,15 +337,22 @@ export const addFieldStatePromise = async ({
|
||||
case 'group': {
|
||||
await iterateFields({
|
||||
id,
|
||||
anyParentLocalized: field.localized || anyParentLocalized,
|
||||
config,
|
||||
data: data?.[field.name] || {},
|
||||
fields: field.fields,
|
||||
filter,
|
||||
forceFullValue,
|
||||
fullData,
|
||||
includeSchema,
|
||||
locale,
|
||||
omitParents,
|
||||
operation,
|
||||
parentPassesCondition: passesCondition,
|
||||
path: `${path}${field.name}.`,
|
||||
preferences,
|
||||
skipConditionChecks,
|
||||
skipValidation,
|
||||
state,
|
||||
t,
|
||||
user,
|
||||
@@ -324,7 +412,9 @@ export const addFieldStatePromise = async ({
|
||||
fieldState.initialValue = relationshipValue
|
||||
}
|
||||
|
||||
state[`${path}${field.name}`] = fieldState
|
||||
if (!filter || filter(args)) {
|
||||
state[`${path}${field.name}`] = fieldState
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
@@ -337,7 +427,9 @@ export const addFieldStatePromise = async ({
|
||||
fieldState.value = relationshipValue
|
||||
fieldState.initialValue = relationshipValue
|
||||
|
||||
state[`${path}${field.name}`] = fieldState
|
||||
if (!filter || filter(args)) {
|
||||
state[`${path}${field.name}`] = fieldState
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
@@ -347,7 +439,9 @@ export const addFieldStatePromise = async ({
|
||||
fieldState.initialValue = valueWithDefault
|
||||
|
||||
// Add field to state
|
||||
state[`${path}${field.name}`] = fieldState
|
||||
if (!filter || filter(args)) {
|
||||
state[`${path}${field.name}`] = fieldState
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
@@ -356,15 +450,22 @@ export const addFieldStatePromise = async ({
|
||||
// Handle field types that do not use names (row, etc)
|
||||
await iterateFields({
|
||||
id,
|
||||
anyParentLocalized: field.localized || anyParentLocalized,
|
||||
config,
|
||||
data,
|
||||
fields: field.fields,
|
||||
filter,
|
||||
forceFullValue,
|
||||
fullData,
|
||||
includeSchema,
|
||||
locale,
|
||||
omitParents,
|
||||
operation,
|
||||
parentPassesCondition: passesCondition,
|
||||
path,
|
||||
preferences,
|
||||
skipConditionChecks,
|
||||
skipValidation,
|
||||
state,
|
||||
t,
|
||||
user,
|
||||
@@ -373,15 +474,22 @@ export const addFieldStatePromise = async ({
|
||||
const promises = field.tabs.map((tab) =>
|
||||
iterateFields({
|
||||
id,
|
||||
anyParentLocalized: tab.localized || anyParentLocalized,
|
||||
config,
|
||||
data: tabHasName(tab) ? data?.[tab.name] : data,
|
||||
fields: tab.fields,
|
||||
filter,
|
||||
forceFullValue,
|
||||
fullData,
|
||||
includeSchema,
|
||||
locale,
|
||||
omitParents,
|
||||
operation,
|
||||
parentPassesCondition: passesCondition,
|
||||
path: tabHasName(tab) ? `${path}${tab.name}.` : path,
|
||||
preferences,
|
||||
skipConditionChecks,
|
||||
skipValidation,
|
||||
state,
|
||||
t,
|
||||
user,
|
||||
|
||||
@@ -4,65 +4,123 @@ import type { User } from '../../../../../auth'
|
||||
import type { SanitizedConfig } from '../../../../../config/types'
|
||||
import type { Field as FieldSchema } from '../../../../../fields/config/types'
|
||||
import type { Data, Fields } from '../types'
|
||||
import type { AddFieldStatePromiseArgs } from './addFieldStatePromise'
|
||||
|
||||
import { fieldIsPresentationalOnly } from '../../../../../fields/config/types'
|
||||
import { addFieldStatePromise } from './addFieldStatePromise'
|
||||
|
||||
type Args = {
|
||||
config: SanitizedConfig
|
||||
/**
|
||||
* if any parents is localized, then the field is localized. @default false
|
||||
*/
|
||||
anyParentLocalized?: boolean
|
||||
/**
|
||||
* config is only needed for validation
|
||||
*/
|
||||
config?: SanitizedConfig
|
||||
data: Data
|
||||
fields: FieldSchema[]
|
||||
filter?: (args: AddFieldStatePromiseArgs) => boolean
|
||||
/**
|
||||
* Force the value of fields like arrays or blocks to be the full value instead of the length @default false
|
||||
*/
|
||||
forceFullValue?: boolean
|
||||
fullData: Data
|
||||
id: number | string
|
||||
id?: number | string
|
||||
/**
|
||||
* Whether the field schema should be included in the state. @default false
|
||||
*/
|
||||
includeSchema?: boolean
|
||||
|
||||
/**
|
||||
* operation is only needed for checking field conditions
|
||||
*/
|
||||
locale: string
|
||||
/**
|
||||
* Whether to omit parent fields in the state. @default false
|
||||
*/
|
||||
omitParents?: boolean
|
||||
/**
|
||||
* operation is only needed for validation
|
||||
*/
|
||||
operation: 'create' | 'update'
|
||||
parentPassesCondition: boolean
|
||||
path: string
|
||||
preferences: {
|
||||
parentPassesCondition?: boolean
|
||||
/**
|
||||
* The initial path of the field. @default ''
|
||||
*/
|
||||
path?: string
|
||||
preferences?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
state: Fields
|
||||
/**
|
||||
* Whether to skip checking the field's condition. @default false
|
||||
*/
|
||||
skipConditionChecks?: boolean
|
||||
/**
|
||||
* Whether to skip validating the field. @default false
|
||||
*/
|
||||
skipValidation?: boolean
|
||||
state?: Fields
|
||||
t: TFunction
|
||||
user: User
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens the fields schema and fields data
|
||||
*/
|
||||
export const iterateFields = async ({
|
||||
id,
|
||||
anyParentLocalized = false,
|
||||
config,
|
||||
data,
|
||||
fields,
|
||||
filter,
|
||||
forceFullValue = false,
|
||||
fullData,
|
||||
includeSchema = false,
|
||||
locale,
|
||||
omitParents = false,
|
||||
operation,
|
||||
parentPassesCondition,
|
||||
parentPassesCondition = true,
|
||||
path = '',
|
||||
preferences,
|
||||
state,
|
||||
skipConditionChecks = false,
|
||||
skipValidation = false,
|
||||
state = {},
|
||||
t,
|
||||
user,
|
||||
}: Args): Promise<void> => {
|
||||
const promises = []
|
||||
fields.forEach((field) => {
|
||||
const initialData = data
|
||||
if (!fieldIsPresentationalOnly(field) && !field?.admin?.disabled) {
|
||||
const passesCondition = Boolean(
|
||||
(field?.admin?.condition
|
||||
? Boolean(field.admin.condition(fullData || {}, initialData || {}, { user }))
|
||||
: true) && parentPassesCondition,
|
||||
)
|
||||
let passesCondition = true
|
||||
if (!skipConditionChecks) {
|
||||
passesCondition = Boolean(
|
||||
(field?.admin?.condition
|
||||
? Boolean(field.admin.condition(fullData || {}, data || {}, { user }))
|
||||
: true) && parentPassesCondition,
|
||||
)
|
||||
}
|
||||
|
||||
promises.push(
|
||||
addFieldStatePromise({
|
||||
id,
|
||||
anyParentLocalized,
|
||||
config,
|
||||
data,
|
||||
field,
|
||||
filter,
|
||||
forceFullValue,
|
||||
fullData,
|
||||
includeSchema,
|
||||
locale,
|
||||
omitParents,
|
||||
operation,
|
||||
passesCondition,
|
||||
path,
|
||||
preferences,
|
||||
skipConditionChecks,
|
||||
skipValidation,
|
||||
state,
|
||||
t,
|
||||
user,
|
||||
|
||||
@@ -2,11 +2,22 @@ import { unflatten as flatleyUnflatten } from 'flatley'
|
||||
|
||||
import type { Data, Fields } from './types'
|
||||
|
||||
const reduceFieldsToValues = (fields: Fields, unflatten?: boolean): Data => {
|
||||
/**
|
||||
* Reduce flattened form fields (Fields) to just map to the respective values instead of the full FormField object
|
||||
*
|
||||
* @param unflatten This also unflattens the data if `unflatten` is true. The unflattened data should match the original data structure
|
||||
* @param ignoreDisableFormData - if true, will include fields that have `disableFormData` set to true, for example, blocks or arrays fields.
|
||||
*
|
||||
*/
|
||||
const reduceFieldsToValues = (
|
||||
fields: Fields,
|
||||
unflatten?: boolean,
|
||||
ignoreDisableFormData?: boolean,
|
||||
): Data => {
|
||||
const data = {}
|
||||
|
||||
Object.keys(fields).forEach((key) => {
|
||||
if (!fields[key].disableFormData) {
|
||||
if (ignoreDisableFormData === true || !fields[key].disableFormData) {
|
||||
data[key] = fields[key].value
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@ export type FormField = {
|
||||
condition?: Condition
|
||||
disableFormData?: boolean
|
||||
errorMessage?: string
|
||||
fieldSchema?: FieldConfig
|
||||
initialValue: unknown
|
||||
passesCondition?: boolean
|
||||
rows?: Row[]
|
||||
|
||||
@@ -29,9 +29,14 @@ type RichTextAdapterBase<
|
||||
}) => Promise<void> | null
|
||||
outputSchema?: ({
|
||||
field,
|
||||
interfaceNameDefinitions,
|
||||
isRequired,
|
||||
}: {
|
||||
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
|
||||
/**
|
||||
* Allows you to define new top-level interfaces that can be re-used in the output schema.
|
||||
*/
|
||||
interfaceNameDefinitions: Map<string, JSONSchema4>
|
||||
isRequired: boolean
|
||||
}) => JSONSchema4
|
||||
populationPromise?: (data: {
|
||||
|
||||
@@ -137,6 +137,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
fieldBaseClass,
|
||||
baseClass,
|
||||
className,
|
||||
`field-${path.replace(/\./g, '__')}`,
|
||||
showError && 'error',
|
||||
readOnly && 'read-only',
|
||||
]
|
||||
|
||||
@@ -40,7 +40,6 @@ export const DocumentInfoProvider: React.FC<Props> = ({
|
||||
const [publishedDoc, setPublishedDoc] = useState<TypeWithID & TypeWithTimestamps>(null)
|
||||
const [versions, setVersions] = useState<PaginatedDocs<Version>>(null)
|
||||
const [unpublishedVersions, setUnpublishedVersions] = useState<PaginatedDocs<Version>>(null)
|
||||
const [docPermissions, setDocPermissions] = useState<DocumentPermissions>(null)
|
||||
|
||||
const baseURL = `${serverURL}${api}`
|
||||
let slug: string
|
||||
@@ -62,6 +61,10 @@ export const DocumentInfoProvider: React.FC<Props> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const [docPermissions, setDocPermissions] = useState<DocumentPermissions>(
|
||||
permissions[pluralType][slug],
|
||||
)
|
||||
|
||||
const getVersions = useCallback(async () => {
|
||||
let versionFetchURL
|
||||
let publishedFetchURL
|
||||
@@ -215,14 +218,14 @@ export const DocumentInfoProvider: React.FC<Props> = ({
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
})
|
||||
const json = await res.json()
|
||||
setDocPermissions(json)
|
||||
} else {
|
||||
// fallback to permissions from the entity type
|
||||
// (i.e. create has no id)
|
||||
setDocPermissions(permissions[pluralType][slug])
|
||||
try {
|
||||
const json = await res.json()
|
||||
setDocPermissions(json)
|
||||
} catch (e) {
|
||||
console.error('Unable to fetch document permissions', e)
|
||||
}
|
||||
}
|
||||
}, [serverURL, api, pluralType, slug, id, permissions, i18n.language, code])
|
||||
}, [serverURL, api, pluralType, slug, id, i18n.language, code])
|
||||
|
||||
const getDocPreferences = useCallback(async () => {
|
||||
return getPreference<DocumentPreferences>(preferencesKey)
|
||||
@@ -262,6 +265,7 @@ export const DocumentInfoProvider: React.FC<Props> = ({
|
||||
|
||||
const value: ContextType = {
|
||||
id,
|
||||
slug,
|
||||
collection,
|
||||
docPermissions,
|
||||
getDocPermissions,
|
||||
@@ -271,7 +275,6 @@ export const DocumentInfoProvider: React.FC<Props> = ({
|
||||
preferencesKey,
|
||||
publishedDoc,
|
||||
setDocFieldPreferences,
|
||||
slug,
|
||||
unpublishedVersions,
|
||||
versions,
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { TypeWithVersion } from '../../../../versions/types'
|
||||
|
||||
export type Version = TypeWithVersion<any>
|
||||
|
||||
export type DocumentPermissions = CollectionPermission | GlobalPermission | null
|
||||
export type DocumentPermissions = CollectionPermission | GlobalPermission
|
||||
|
||||
export type ContextType = {
|
||||
collection?: SanitizedCollectionConfig
|
||||
|
||||
@@ -17,9 +17,9 @@ export type globalViewType =
|
||||
| 'Version'
|
||||
| 'Versions'
|
||||
|
||||
export const defaultGlobalViews: {
|
||||
export const defaultGlobalViews = (): {
|
||||
[key in globalViewType]: React.ComponentType<any>
|
||||
} = {
|
||||
} => ({
|
||||
API,
|
||||
Default: DefaultGlobalEdit,
|
||||
LivePreview: LivePreviewView,
|
||||
@@ -27,7 +27,7 @@ export const defaultGlobalViews: {
|
||||
Relationships: null,
|
||||
Version: VersionView,
|
||||
Versions: VersionsView,
|
||||
}
|
||||
})
|
||||
|
||||
export const CustomGlobalComponent = (
|
||||
args: GlobalEditViewProps & {
|
||||
@@ -43,18 +43,14 @@ export const CustomGlobalComponent = (
|
||||
// For example, the Edit view:
|
||||
// 1. Edit?.Default
|
||||
// 2. Edit?.Default?.Component
|
||||
// TODO: Remove the `@ts-ignore` when a Typescript wizard arrives
|
||||
// For some reason `Component` does not exist on type `Edit[view]` no matter how narrow the type is
|
||||
const Component =
|
||||
typeof Edit === 'object' && typeof Edit[view] === 'function'
|
||||
? Edit[view]
|
||||
: typeof Edit === 'object' &&
|
||||
typeof Edit?.[view] === 'object' &&
|
||||
// @ts-ignore
|
||||
typeof Edit[view].Component === 'function'
|
||||
? // @ts-ignore
|
||||
Edit[view].Component
|
||||
: defaultGlobalViews[view]
|
||||
? Edit[view].Component
|
||||
: defaultGlobalViews()[view]
|
||||
|
||||
if (Component) {
|
||||
return <Component {...args} />
|
||||
|
||||
@@ -17,9 +17,9 @@ export type collectionViewType =
|
||||
| 'Version'
|
||||
| 'Versions'
|
||||
|
||||
export const defaultCollectionViews: {
|
||||
export const defaultCollectionViews = (): {
|
||||
[key in collectionViewType]: React.ComponentType<any>
|
||||
} = {
|
||||
} => ({
|
||||
API,
|
||||
Default: DefaultCollectionEdit,
|
||||
LivePreview: LivePreviewView,
|
||||
@@ -27,7 +27,7 @@ export const defaultCollectionViews: {
|
||||
Relationships: null,
|
||||
Version: VersionView,
|
||||
Versions: VersionsView,
|
||||
}
|
||||
})
|
||||
|
||||
export const CustomCollectionComponent = (
|
||||
args: CollectionEditViewProps & {
|
||||
@@ -43,18 +43,15 @@ export const CustomCollectionComponent = (
|
||||
// For example, the Edit view:
|
||||
// 1. Edit?.Default
|
||||
// 2. Edit?.Default?.Component
|
||||
// TODO: Remove the `@ts-ignore` when a Typescript wizard arrives
|
||||
// For some reason `Component` does not exist on type `Edit[view]` no matter how narrow the type is
|
||||
|
||||
const Component =
|
||||
typeof Edit === 'object' && typeof Edit[view] === 'function'
|
||||
? Edit[view]
|
||||
: typeof Edit === 'object' &&
|
||||
typeof Edit?.[view] === 'object' &&
|
||||
// @ts-ignore
|
||||
typeof Edit[view].Component === 'function'
|
||||
? // @ts-ignore
|
||||
Edit[view].Component
|
||||
: defaultCollectionViews[view]
|
||||
? Edit[view].Component
|
||||
: defaultCollectionViews()[view]
|
||||
|
||||
if (Component) {
|
||||
return <Component {...args} />
|
||||
|
||||
@@ -18,8 +18,8 @@ import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'
|
||||
import isLocked from '../isLocked'
|
||||
import { authenticateLocalStrategy } from '../strategies/local/authenticate'
|
||||
import { incrementLoginAttempts } from '../strategies/local/incrementLoginAttempts'
|
||||
import { resetLoginAttempts } from '../strategies/local/resetLoginAttempts'
|
||||
import { getFieldsToSign } from './getFieldsToSign'
|
||||
import unlock from './unlock'
|
||||
|
||||
export type Result = {
|
||||
exp?: number
|
||||
@@ -115,16 +115,16 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
})
|
||||
}
|
||||
|
||||
if (shouldCommit) await commitTransaction(req)
|
||||
|
||||
throw new AuthenticationError(req.t)
|
||||
}
|
||||
|
||||
if (maxLoginAttemptsEnabled) {
|
||||
await unlock({
|
||||
collection: {
|
||||
config: collectionConfig,
|
||||
},
|
||||
data,
|
||||
overrideAccess: true,
|
||||
await resetLoginAttempts({
|
||||
collection: collectionConfig,
|
||||
doc: user,
|
||||
payload: req.payload,
|
||||
req,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,5 +52,6 @@ export const incrementLoginAttempts = async ({
|
||||
id: doc.id,
|
||||
collection: collection.slug,
|
||||
data,
|
||||
req,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export const resetLoginAttempts = async ({
|
||||
payload,
|
||||
req,
|
||||
}: Args): Promise<void> => {
|
||||
if (!('lockUntil' in doc && typeof doc.lockUntil === 'string') || doc.loginAttempts === 0) return
|
||||
await payload.update({
|
||||
id: doc.id,
|
||||
collection: collection.slug,
|
||||
@@ -22,6 +23,7 @@ export const resetLoginAttempts = async ({
|
||||
lockUntil: null,
|
||||
loginAttempts: 0,
|
||||
},
|
||||
overrideAccess: true,
|
||||
req,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ export async function generateTypes(): Promise<void> {
|
||||
style: {
|
||||
singleQuote: true,
|
||||
},
|
||||
// Generates code for $defs that aren't referenced by the schema. Reason:
|
||||
// If a field defines an interfaceName, it should be included in the generated types
|
||||
// even if it's not used by another type. Reason: the user might want to use it in their own code.
|
||||
unreachableDefinitions: true,
|
||||
}).then((compiled) => {
|
||||
if (config.typescript.declare !== false) {
|
||||
compiled += `\n\n${declare}`
|
||||
|
||||
@@ -27,7 +27,7 @@ const availableCommands = [
|
||||
const availableCommandsMsg = `Available commands: ${availableCommands.join(', ')}`
|
||||
|
||||
export const migrate = async (parsedArgs: ParsedArgs): Promise<void> => {
|
||||
const { _: args, file, help } = parsedArgs
|
||||
const { _: args, file, forceAcceptWarning, help } = parsedArgs
|
||||
if (help) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`\n\n${availableCommandsMsg}\n`) // Avoid having to init payload to get the logger
|
||||
@@ -74,11 +74,16 @@ export const migrate = async (parsedArgs: ParsedArgs): Promise<void> => {
|
||||
await adapter.migrateReset()
|
||||
break
|
||||
case 'migrate:fresh':
|
||||
await adapter.migrateFresh()
|
||||
await adapter.migrateFresh({ forceAcceptWarning })
|
||||
break
|
||||
case 'migrate:create':
|
||||
try {
|
||||
await adapter.createMigration({ file, migrationName: args[1], payload })
|
||||
await adapter.createMigration({
|
||||
file,
|
||||
forceAcceptWarning,
|
||||
migrationName: args[1],
|
||||
payload,
|
||||
})
|
||||
} catch (err) {
|
||||
throw new Error(`Error creating migration: ${err.message}`)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import httpStatus from 'http-status'
|
||||
|
||||
import type { FindOneArgs } from '../../database/types'
|
||||
import type { PayloadRequest } from '../../express/types'
|
||||
import type { Collection, TypeWithID, BeforeOperationHook } from '../config/types'
|
||||
import type { Collection, TypeWithID } from '../config/types'
|
||||
|
||||
import executeAccess from '../../auth/executeAccess'
|
||||
import { hasWhereAccessResult } from '../../auth/types'
|
||||
@@ -45,26 +45,6 @@ async function restoreVersion<T extends TypeWithID = any>(args: Arguments): Prom
|
||||
throw new APIError('Missing ID of version to restore.', httpStatus.BAD_REQUEST)
|
||||
}
|
||||
|
||||
// WIP: https://github.com/payloadcms/payload/discussions/4901
|
||||
// /////////////////////////////////////
|
||||
// beforeOperation - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
await args.collection.config.hooks.beforeOperation.reduce(
|
||||
async (priorHook: BeforeOperationHook | Promise<void>, hook: BeforeOperationHook) => {
|
||||
await priorHook
|
||||
|
||||
args =
|
||||
(await hook({
|
||||
args,
|
||||
collection: args.collection.config,
|
||||
context: args.req.context,
|
||||
operation: 'update',
|
||||
})) || args
|
||||
},
|
||||
Promise.resolve(),
|
||||
)
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Retrieve original raw version
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -26,6 +26,7 @@ import type { PayloadRequest } from '../express/types'
|
||||
import type { GlobalConfig, SanitizedGlobalConfig } from '../globals/config/types'
|
||||
import type { Payload } from '../payload'
|
||||
import type { Where } from '../types'
|
||||
import type { PayloadLogger } from '../utilities/logger'
|
||||
|
||||
type Prettify<T> = {
|
||||
[K in keyof T]: T[K]
|
||||
@@ -155,6 +156,11 @@ export type InitOptions = {
|
||||
* See Pino Docs for options: https://getpino.io/#/docs/api?id=options
|
||||
*/
|
||||
loggerOptions?: LoggerOptions
|
||||
/**
|
||||
* A previously instantiated logger instance. Must conform to the PayloadLogger interface which uses Pino
|
||||
* This allows you to bring your own logger instance and let payload use it
|
||||
*/
|
||||
logger?: PayloadLogger
|
||||
|
||||
/**
|
||||
* A function that is called immediately following startup that receives the Payload instance as it's only argument.
|
||||
|
||||
@@ -39,7 +39,7 @@ export function createDatabaseAdapter<T extends BaseDatabaseAdapter>(
|
||||
createMigration,
|
||||
migrate,
|
||||
migrateDown,
|
||||
migrateFresh: async () => null,
|
||||
migrateFresh: async ({ forceAcceptWarning = null }) => null,
|
||||
migrateRefresh,
|
||||
migrateReset,
|
||||
migrateStatus,
|
||||
|
||||
@@ -78,7 +78,7 @@ export interface BaseDatabaseAdapter {
|
||||
/**
|
||||
* Drop the current database and run all migrate up functions
|
||||
*/
|
||||
migrateFresh: () => Promise<void>
|
||||
migrateFresh: (args: { forceAcceptWarning?: boolean }) => Promise<void>
|
||||
/**
|
||||
* Run all migration down functions before running up
|
||||
*/
|
||||
@@ -138,6 +138,10 @@ export type Destroy = (payload: Payload) => Promise<void>
|
||||
|
||||
export type CreateMigration = (args: {
|
||||
file?: string
|
||||
/**
|
||||
* Skips the prompt asking to create empty migrations
|
||||
*/
|
||||
forceAcceptWarning?: boolean
|
||||
migrationName?: string
|
||||
payload: Payload
|
||||
}) => Promise<void>
|
||||
|
||||
@@ -9,6 +9,7 @@ export { combineMerge } from '../utilities/combineMerge'
|
||||
export {
|
||||
configToJSONSchema,
|
||||
entityToJSONSchema,
|
||||
fieldsToJSONSchema,
|
||||
withNullableJSONSchemaType,
|
||||
} from '../utilities/configToJSONSchema'
|
||||
export { createArrayFromCommaDelineated } from '../utilities/createArrayFromCommaDelineated'
|
||||
|
||||
@@ -38,8 +38,9 @@ export type FieldHookArgs<T extends TypeWithID = any, P = any, S = any> = {
|
||||
originalDoc?: T
|
||||
/** The document before changes were applied, only in `afterChange` hooks. */
|
||||
previousDoc?: T
|
||||
/** The sibling data from the previous document in `afterChange` hook. */
|
||||
/** The sibling data of the document before changes being applied, only in `beforeChange` and `afterChange` hook. */
|
||||
previousSiblingDoc?: T
|
||||
/** The previous value of the field, before changes, only in `beforeChange` and `afterChange` hooks. */
|
||||
previousValue?: P
|
||||
/** The Express request object. It is mocked for Local API operations. */
|
||||
req: PayloadRequest
|
||||
|
||||
@@ -88,6 +88,8 @@ export const promise = async ({
|
||||
global,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
siblingData,
|
||||
value: siblingData[field.name],
|
||||
|
||||
@@ -311,7 +311,8 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
||||
*/
|
||||
// @ts-expect-error // TODO: TypeScript hallucinating again. fix later
|
||||
async init(options: InitOptions): Promise<Payload> {
|
||||
this.logger = Logger('payload', options.loggerOptions, options.loggerDestination)
|
||||
this.logger =
|
||||
options.logger ?? Logger('payload', options.loggerOptions, options.loggerDestination)
|
||||
|
||||
if (!options.secret) {
|
||||
throw new Error('Error: missing secret key. A secret key is needed to secure Payload.')
|
||||
|
||||
@@ -12,6 +12,7 @@ const preferenceAccess: Access = ({ req }) => ({
|
||||
})
|
||||
|
||||
const getPreferencesCollection = (config: Config): CollectionConfig => ({
|
||||
slug: 'payload-preferences',
|
||||
access: {
|
||||
delete: preferenceAccess,
|
||||
read: preferenceAccess,
|
||||
@@ -39,6 +40,7 @@ const getPreferencesCollection = (config: Config): CollectionConfig => ({
|
||||
fields: [
|
||||
{
|
||||
name: 'user',
|
||||
type: 'relationship',
|
||||
hooks: {
|
||||
beforeValidate: [
|
||||
({ req }) => {
|
||||
@@ -52,22 +54,22 @@ const getPreferencesCollection = (config: Config): CollectionConfig => ({
|
||||
},
|
||||
],
|
||||
},
|
||||
index: true,
|
||||
relationTo: config.collections
|
||||
.filter((collectionConfig) => collectionConfig.auth)
|
||||
.map((collectionConfig) => collectionConfig.slug),
|
||||
required: true,
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
name: 'key',
|
||||
type: 'text',
|
||||
index: true,
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
type: 'json',
|
||||
},
|
||||
],
|
||||
slug: 'payload-preferences',
|
||||
})
|
||||
|
||||
export default getPreferencesCollection
|
||||
|
||||
@@ -78,9 +78,12 @@ export function withNullableJSONSchemaType(
|
||||
return fieldTypes
|
||||
}
|
||||
|
||||
function fieldsToJSONSchema(
|
||||
export function fieldsToJSONSchema(
|
||||
collectionIDFieldTypes: { [key: string]: 'number' | 'string' },
|
||||
fields: Field[],
|
||||
/**
|
||||
* Allows you to define new top-level interfaces that can be re-used in the output schema.
|
||||
*/
|
||||
interfaceNameDefinitions: Map<string, JSONSchema4>,
|
||||
): {
|
||||
properties: {
|
||||
@@ -144,6 +147,7 @@ function fieldsToJSONSchema(
|
||||
if (field.editor.outputSchema) {
|
||||
fieldSchema = field.editor.outputSchema({
|
||||
field,
|
||||
interfaceNameDefinitions,
|
||||
isRequired,
|
||||
})
|
||||
} else {
|
||||
@@ -524,8 +528,11 @@ export function configToJSONSchema(
|
||||
config: SanitizedConfig,
|
||||
defaultIDType?: 'number' | 'text',
|
||||
): JSONSchema4 {
|
||||
// a mutable Map to store custom top-level `interfaceName` types
|
||||
// a mutable Map to store custom top-level `interfaceName` types. Fields with an `interfaceName` property will be moved to the top-level definitions here
|
||||
const interfaceNameDefinitions: Map<string, JSONSchema4> = new Map()
|
||||
|
||||
// Collections and Globals have to be moved to the top-level definitions as well. Reason: The top-level type will be the `Config` type - we don't want all collection and global
|
||||
// types to be inlined inside the `Config` type
|
||||
const entityDefinitions: { [k: string]: JSONSchema4 } = [
|
||||
...config.globals,
|
||||
...config.collections,
|
||||
@@ -537,6 +544,7 @@ export function configToJSONSchema(
|
||||
return {
|
||||
additionalProperties: false,
|
||||
definitions: { ...entityDefinitions, ...Object.fromEntries(interfaceNameDefinitions) },
|
||||
// These properties here will be very simple, as all the complexity is in the definitions. These are just the properties for the top-level `Config` type
|
||||
properties: {
|
||||
collections: generateEntitySchemas(config.collections || []),
|
||||
globals: generateEntitySchemas(config.globals || []),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -10,6 +10,9 @@ export async function getFilePrefix({
|
||||
const imageSizes = (collection?.upload as IncomingUploadType)?.imageSizes || []
|
||||
const files = await req.payload.find({
|
||||
collection: collection.slug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
where: {
|
||||
or: [
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"classnames": "^2.3.2",
|
||||
"deep-equal": "2.2.3",
|
||||
"i18next": "22.5.1",
|
||||
"json-schema": "^0.4.0",
|
||||
"lexical": "0.12.6",
|
||||
"lodash": "4.17.21",
|
||||
"react": "18.2.0",
|
||||
@@ -42,6 +43,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/json-schema": "7.0.12",
|
||||
"@types/node": "20.6.2",
|
||||
"@types/react": "18.2.15",
|
||||
"payload": "workspace:*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Block } from 'payload/types'
|
||||
import type { Block, BlockField } from 'payload/types'
|
||||
|
||||
import { baseBlockFields } from 'payload/config'
|
||||
import { formatLabels, getTranslation } from 'payload/utilities'
|
||||
import { fieldsToJSONSchema, formatLabels, getTranslation } from 'payload/utilities'
|
||||
|
||||
import type { FeatureProvider } from '../types'
|
||||
|
||||
@@ -31,6 +31,20 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
generatedTypes: {
|
||||
modifyOutputSchema: ({ currentSchema, field, interfaceNameDefinitions }) => {
|
||||
const blocksField: BlockField = {
|
||||
name: field?.name + '_lexical_blocks',
|
||||
blocks: props.blocks,
|
||||
type: 'blocks',
|
||||
}
|
||||
// This is only done so that interfaceNameDefinitions sets those block's interfaceNames.
|
||||
// we don't actually use the JSON Schema itself in the generated types yet.
|
||||
fieldsToJSONSchema({}, [blocksField], interfaceNameDefinitions)
|
||||
|
||||
return currentSchema
|
||||
},
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
node: BlockNode,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Transformer } from '@lexical/markdown'
|
||||
import type { JSONSchema4 } from 'json-schema'
|
||||
import type { Klass, LexicalEditor, LexicalNode, SerializedEditorState } from 'lexical'
|
||||
import type { SerializedLexicalNode } from 'lexical'
|
||||
import type { LexicalNodeReplacement } from 'lexical'
|
||||
@@ -65,6 +66,25 @@ export type Feature = {
|
||||
floatingSelectToolbar?: {
|
||||
sections: FloatingToolbarSection[]
|
||||
}
|
||||
generatedTypes?: {
|
||||
modifyOutputSchema: ({
|
||||
currentSchema,
|
||||
field,
|
||||
interfaceNameDefinitions,
|
||||
isRequired,
|
||||
}: {
|
||||
/**
|
||||
* Current schema which will be modified by this function.
|
||||
*/
|
||||
currentSchema: JSONSchema4
|
||||
field: RichTextField<SerializedEditorState, AdapterProps>
|
||||
/**
|
||||
* Allows you to define new top-level interfaces that can be re-used in the output schema.
|
||||
*/
|
||||
interfaceNameDefinitions: Map<string, JSONSchema4>
|
||||
isRequired: boolean
|
||||
}) => JSONSchema4
|
||||
}
|
||||
hooks?: {
|
||||
afterReadPromise?: ({
|
||||
field,
|
||||
@@ -200,6 +220,27 @@ export type SanitizedFeatures = Required<
|
||||
floatingSelectToolbar: {
|
||||
sections: FloatingToolbarSection[]
|
||||
}
|
||||
generatedTypes: {
|
||||
modifyOutputSchemas: Array<
|
||||
({
|
||||
currentSchema,
|
||||
field,
|
||||
interfaceNameDefinitions,
|
||||
isRequired,
|
||||
}: {
|
||||
/**
|
||||
* Current schema which will be modified by this function.
|
||||
*/
|
||||
currentSchema: JSONSchema4
|
||||
field: RichTextField<SerializedEditorState, AdapterProps>
|
||||
/**
|
||||
* Allows you to define new top-level interfaces that can be re-used in the output schema.
|
||||
*/
|
||||
interfaceNameDefinitions: Map<string, JSONSchema4>
|
||||
isRequired: boolean
|
||||
}) => JSONSchema4
|
||||
>
|
||||
}
|
||||
hooks: {
|
||||
afterReadPromises: Array<
|
||||
({
|
||||
|
||||
@@ -12,6 +12,9 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
||||
floatingSelectToolbar: {
|
||||
sections: [],
|
||||
},
|
||||
generatedTypes: {
|
||||
modifyOutputSchemas: [],
|
||||
},
|
||||
hooks: {
|
||||
afterReadPromises: [],
|
||||
load: [],
|
||||
@@ -29,6 +32,9 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
||||
}
|
||||
|
||||
features.forEach((feature) => {
|
||||
if (feature?.generatedTypes?.modifyOutputSchema) {
|
||||
sanitized.generatedTypes.modifyOutputSchemas.push(feature.generatedTypes.modifyOutputSchema)
|
||||
}
|
||||
if (feature.hooks) {
|
||||
if (feature.hooks.afterReadPromise) {
|
||||
sanitized.hooks.afterReadPromises = sanitized.hooks.afterReadPromises.concat(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JSONSchema4 } from 'json-schema'
|
||||
import type { SerializedEditorState } from 'lexical'
|
||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
|
||||
import type { RichTextAdapter } from 'payload/types'
|
||||
@@ -98,8 +99,8 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
})
|
||||
},
|
||||
editorConfig: finalSanitizedEditorConfig,
|
||||
outputSchema: ({ isRequired }) => {
|
||||
return {
|
||||
outputSchema: ({ field, interfaceNameDefinitions, isRequired }) => {
|
||||
let outputSchema: JSONSchema4 = {
|
||||
// This schema matches the SerializedEditorState type so far, that it's possible to cast SerializedEditorState to this schema without any errors.
|
||||
// In the future, we should
|
||||
// 1) allow recursive children
|
||||
@@ -155,6 +156,17 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
required: ['root'],
|
||||
type: withNullableJSONSchemaType('object', isRequired),
|
||||
}
|
||||
for (const modifyOutputSchema of finalSanitizedEditorConfig.features.generatedTypes
|
||||
.modifyOutputSchemas) {
|
||||
outputSchema = modifyOutputSchema({
|
||||
currentSchema: outputSchema,
|
||||
field,
|
||||
interfaceNameDefinitions,
|
||||
isRequired,
|
||||
})
|
||||
}
|
||||
|
||||
return outputSchema
|
||||
},
|
||||
populationPromise({
|
||||
context,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-slate",
|
||||
"version": "1.3.1",
|
||||
"version": "1.4.0",
|
||||
"description": "The officially supported Slate richtext adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
1219
pnpm-lock.yaml
generated
1219
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ export const clearUserCart: AfterChangeHook<Order> = async ({ doc, req, operatio
|
||||
const { payload } = req
|
||||
|
||||
if (operation === 'create' && doc.orderedBy) {
|
||||
const orderedBy = typeof doc.orderedBy === 'string' ? doc.orderedBy : doc.orderedBy.id
|
||||
const orderedBy = typeof doc.orderedBy === 'object' ? doc.orderedBy.id : doc.orderedBy
|
||||
|
||||
const user = await payload.findByID({
|
||||
collection: 'users',
|
||||
|
||||
@@ -6,7 +6,7 @@ export const updateUserPurchases: AfterChangeHook<Order> = async ({ doc, req, op
|
||||
const { payload } = req
|
||||
|
||||
if ((operation === 'create' || operation === 'update') && doc.orderedBy && doc.items) {
|
||||
const orderedBy = typeof doc.orderedBy === 'string' ? doc.orderedBy : doc.orderedBy.id
|
||||
const orderedBy = typeof doc.orderedBy === 'object' ? doc.orderedBy.id : doc.orderedBy
|
||||
|
||||
const user = await payload.findByID({
|
||||
collection: 'users',
|
||||
@@ -20,10 +20,10 @@ export const updateUserPurchases: AfterChangeHook<Order> = async ({ doc, req, op
|
||||
data: {
|
||||
purchases: [
|
||||
...(user?.purchases?.map(purchase =>
|
||||
typeof purchase === 'string' ? purchase : purchase.id,
|
||||
typeof purchase === 'object' ? purchase.id : purchase,
|
||||
) || []), // eslint-disable-line function-paren-newline
|
||||
...(doc?.items?.map(({ product }) =>
|
||||
typeof product === 'string' ? product : product.id,
|
||||
typeof product === 'object' ? product.id : product,
|
||||
) || []), // eslint-disable-line function-paren-newline
|
||||
],
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ export const resolveDuplicatePurchases: FieldHook<User> = async ({ value, operat
|
||||
if ((operation === 'create' || operation === 'update') && value) {
|
||||
return Array.from(
|
||||
new Set(
|
||||
value?.map(purchase => (typeof purchase === 'string' ? purchase : purchase.id)) || [],
|
||||
value?.map(purchase => (typeof purchase === 'object' ? purchase.id : purchase)) || [],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import { usersCollectionSlug } from '../slugs'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: usersCollectionSlug,
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
auth: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'textField',
|
||||
|
||||
@@ -16,13 +16,21 @@ const bundlerAdapters = {
|
||||
webpack: webpackBundler(),
|
||||
}
|
||||
|
||||
const [testSuiteDir] = process.argv.slice(4)
|
||||
const migrationDir = path.resolve(
|
||||
(process.env.PAYLOAD_CONFIG_PATH
|
||||
? path.join(process.env.PAYLOAD_CONFIG_PATH, '..')
|
||||
: testSuiteDir) || __dirname,
|
||||
'migrations',
|
||||
)
|
||||
|
||||
const databaseAdapters = {
|
||||
mongoose: mongooseAdapter({
|
||||
migrationDir: path.resolve(__dirname, '../packages/db-mongodb/migrations'),
|
||||
migrationDir,
|
||||
url: 'mongodb://127.0.0.1/payloadtests',
|
||||
}),
|
||||
postgres: postgresAdapter({
|
||||
migrationDir: path.resolve(__dirname, '../packages/db-postgres/migrations'),
|
||||
migrationDir,
|
||||
pool: {
|
||||
connectionString: process.env.POSTGRES_URL || 'postgres://127.0.0.1:5432/payloadtests',
|
||||
},
|
||||
@@ -33,6 +41,7 @@ export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<S
|
||||
const [name] = process.argv.slice(2)
|
||||
|
||||
const config: Config = {
|
||||
db: databaseAdapters[process.env.PAYLOAD_DATABASE || 'mongoose'],
|
||||
editor: slateEditor({}),
|
||||
rateLimit: {
|
||||
max: 9999999999,
|
||||
@@ -40,7 +49,6 @@ export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<S
|
||||
},
|
||||
telemetry: false,
|
||||
...testConfig,
|
||||
db: databaseAdapters[process.env.PAYLOAD_DATABASE || 'mongoose'],
|
||||
}
|
||||
|
||||
config.admin = {
|
||||
|
||||
1
test/database/.gitignore
vendored
Normal file
1
test/database/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
migrations
|
||||
@@ -4,14 +4,16 @@ import { devUser } from '../credentials'
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [
|
||||
{
|
||||
slug: 'posts',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
required: true,
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'throwAfterChange',
|
||||
type: 'checkbox',
|
||||
defaultValue: false,
|
||||
hooks: {
|
||||
afterChange: [
|
||||
@@ -22,17 +24,16 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
},
|
||||
type: 'checkbox',
|
||||
},
|
||||
],
|
||||
slug: 'posts',
|
||||
},
|
||||
{
|
||||
slug: 'relation-a',
|
||||
fields: [
|
||||
{
|
||||
name: 'relationship',
|
||||
relationTo: 'relation-b',
|
||||
type: 'relationship',
|
||||
relationTo: 'relation-b',
|
||||
},
|
||||
{
|
||||
name: 'richText',
|
||||
@@ -43,14 +44,14 @@ export default buildConfigWithDefaults({
|
||||
plural: 'Relation As',
|
||||
singular: 'Relation A',
|
||||
},
|
||||
slug: 'relation-a',
|
||||
},
|
||||
{
|
||||
slug: 'relation-b',
|
||||
fields: [
|
||||
{
|
||||
name: 'relationship',
|
||||
relationTo: 'relation-a',
|
||||
type: 'relationship',
|
||||
relationTo: 'relation-a',
|
||||
},
|
||||
{
|
||||
name: 'richText',
|
||||
@@ -61,7 +62,6 @@ export default buildConfigWithDefaults({
|
||||
plural: 'Relation Bs',
|
||||
singular: 'Relation B',
|
||||
},
|
||||
slug: 'relation-b',
|
||||
},
|
||||
],
|
||||
onInit: async (payload) => {
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { sql } from 'drizzle-orm'
|
||||
import fs from 'fs'
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import path from 'path'
|
||||
|
||||
import type { DrizzleDB } from '../../packages/db-postgres/src/types'
|
||||
import type { TypeWithID } from '../../packages/payload/src/collections/config/types'
|
||||
import type { PayloadRequest } from '../../packages/payload/src/express/types'
|
||||
|
||||
import payload from '../../packages/payload/src'
|
||||
import { migrate } from '../../packages/payload/src/bin/migrate'
|
||||
import { commitTransaction } from '../../packages/payload/src/utilities/commitTransaction'
|
||||
import { initTransaction } from '../../packages/payload/src/utilities/initTransaction'
|
||||
import { devUser } from '../credentials'
|
||||
import { initPayloadTest } from '../helpers/configHelpers'
|
||||
import removeFiles from '../helpers/removeFiles'
|
||||
|
||||
describe('database', () => {
|
||||
let serverURL
|
||||
@@ -35,6 +41,101 @@ describe('database', () => {
|
||||
user = loginResult.user
|
||||
})
|
||||
|
||||
describe('migrations', () => {
|
||||
beforeAll(async () => {
|
||||
if (process.env.PAYLOAD_DROP_DATABASE === 'true' && 'drizzle' in payload.db) {
|
||||
const drizzle = payload.db.drizzle as DrizzleDB
|
||||
// @ts-expect-error drizzle raw sql typing
|
||||
await drizzle.execute(sql`drop schema public cascade;
|
||||
create schema public;`)
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
removeFiles(path.normalize(payload.db.migrationDir))
|
||||
})
|
||||
|
||||
it('should run migrate:create', async () => {
|
||||
const args = {
|
||||
_: ['migrate:create', 'test'],
|
||||
forceAcceptWarning: true,
|
||||
}
|
||||
await migrate(args)
|
||||
|
||||
// read files names in migrationsDir
|
||||
const migrationFile = path.normalize(fs.readdirSync(payload.db.migrationDir)[0])
|
||||
expect(migrationFile).toContain('_test')
|
||||
})
|
||||
|
||||
it('should run migrate', async () => {
|
||||
const args = {
|
||||
_: ['migrate'],
|
||||
}
|
||||
await migrate(args)
|
||||
const { docs } = await payload.find({
|
||||
collection: 'payload-migrations',
|
||||
})
|
||||
const migration = docs[0]
|
||||
expect(migration.name).toContain('_test')
|
||||
expect(migration.batch).toStrictEqual(1)
|
||||
})
|
||||
|
||||
it('should run migrate:status', async () => {
|
||||
let error
|
||||
const args = {
|
||||
_: ['migrate:status'],
|
||||
}
|
||||
try {
|
||||
await migrate(args)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
expect(error).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should run migrate:fresh', async () => {
|
||||
const args = {
|
||||
_: ['migrate:fresh'],
|
||||
forceAcceptWarning: true,
|
||||
}
|
||||
await migrate(args)
|
||||
const { docs } = await payload.find({
|
||||
collection: 'payload-migrations',
|
||||
})
|
||||
const migration = docs[0]
|
||||
expect(migration.name).toContain('_test')
|
||||
expect(migration.batch).toStrictEqual(1)
|
||||
})
|
||||
|
||||
// known issue: https://github.com/payloadcms/payload/issues/4597
|
||||
it.skip('should run migrate:down', async () => {
|
||||
let error
|
||||
const args = {
|
||||
_: ['migrate:down'],
|
||||
}
|
||||
try {
|
||||
await migrate(args)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
expect(error).toBeUndefined()
|
||||
})
|
||||
|
||||
// known issue: https://github.com/payloadcms/payload/issues/4597
|
||||
it.skip('should run migrate:refresh', async () => {
|
||||
let error
|
||||
const args = {
|
||||
_: ['migrate:refresh'],
|
||||
}
|
||||
try {
|
||||
await migrate(args)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
expect(error).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('transactions', () => {
|
||||
describe('local api', () => {
|
||||
it('should commit multiple operations in isolation', async () => {
|
||||
|
||||
@@ -73,6 +73,7 @@ export const TextBlock: Block = {
|
||||
}
|
||||
|
||||
export const RadioButtonsBlock: Block = {
|
||||
interfaceName: 'LexicalBlocksRadioButtonsBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'radioButtons',
|
||||
|
||||
@@ -2,13 +2,14 @@ import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { generateGraphQLSchema } from '../packages/payload/src/bin/generateGraphQLSchema'
|
||||
import { setTestEnvPaths } from './helpers/setTestEnvPaths'
|
||||
|
||||
const [testConfigDir] = process.argv.slice(2)
|
||||
|
||||
let testDir
|
||||
if (testConfigDir) {
|
||||
testDir = path.resolve(__dirname, testConfigDir)
|
||||
setPaths(testDir)
|
||||
setTestEnvPaths(testDir)
|
||||
generateGraphQLSchema()
|
||||
} else {
|
||||
// Generate graphql schema for entire directory
|
||||
@@ -18,17 +19,7 @@ if (testConfigDir) {
|
||||
.filter((f) => f.isDirectory())
|
||||
.forEach((dir) => {
|
||||
const suiteDir = path.resolve(testDir, dir.name)
|
||||
const configFound = setPaths(suiteDir)
|
||||
const configFound = setTestEnvPaths(suiteDir)
|
||||
if (configFound) generateGraphQLSchema()
|
||||
})
|
||||
}
|
||||
|
||||
// Set config path and TS output path using test dir
|
||||
function setPaths(dir) {
|
||||
const configPath = path.resolve(dir, 'config.ts')
|
||||
if (fs.existsSync(configPath)) {
|
||||
process.env.PAYLOAD_CONFIG_PATH = configPath
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@ import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { generateTypes } from '../packages/payload/src/bin/generateTypes'
|
||||
import { setTestEnvPaths } from './helpers/setTestEnvPaths'
|
||||
|
||||
const [testConfigDir] = process.argv.slice(2)
|
||||
|
||||
let testDir
|
||||
if (testConfigDir) {
|
||||
testDir = path.resolve(__dirname, testConfigDir)
|
||||
setPaths(testDir)
|
||||
setTestEnvPaths(testDir)
|
||||
generateTypes()
|
||||
} else {
|
||||
// Generate types for entire directory
|
||||
@@ -18,19 +19,7 @@ if (testConfigDir) {
|
||||
.filter((f) => f.isDirectory())
|
||||
.forEach((dir) => {
|
||||
const suiteDir = path.resolve(testDir, dir.name)
|
||||
const configFound = setPaths(suiteDir)
|
||||
const configFound = setTestEnvPaths(suiteDir)
|
||||
if (configFound) generateTypes()
|
||||
})
|
||||
}
|
||||
|
||||
// Set config path and TS output path using test dir
|
||||
function setPaths(dir) {
|
||||
const configPath = path.resolve(dir, 'config.ts')
|
||||
const outputPath = path.resolve(dir, 'payload-types.ts')
|
||||
if (fs.existsSync(configPath)) {
|
||||
process.env.PAYLOAD_CONFIG_PATH = configPath
|
||||
process.env.PAYLOAD_TS_OUTPUT_PATH = outputPath
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
14
test/helpers/setTestEnvPaths.ts
Normal file
14
test/helpers/setTestEnvPaths.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// Set config path and TS output path using test dir
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export function setTestEnvPaths(dir) {
|
||||
const configPath = path.resolve(dir, 'config.ts')
|
||||
const outputPath = path.resolve(dir, 'payload-types.ts')
|
||||
if (fs.existsSync(configPath)) {
|
||||
process.env.PAYLOAD_CONFIG_PATH = configPath
|
||||
process.env.PAYLOAD_TS_OUTPUT_PATH = outputPath
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -5,25 +5,11 @@ export const hooksSlug = 'hooks'
|
||||
const Hooks: CollectionConfig = {
|
||||
slug: hooksSlug,
|
||||
access: {
|
||||
read: () => true,
|
||||
create: () => true,
|
||||
delete: () => true,
|
||||
read: () => true,
|
||||
update: () => true,
|
||||
},
|
||||
hooks: {
|
||||
beforeValidate: [({ data }) => validateHookOrder('collectionBeforeValidate', data)],
|
||||
beforeChange: [({ data }) => validateHookOrder('collectionBeforeChange', data)],
|
||||
afterChange: [
|
||||
({ doc, previousDoc }) => {
|
||||
if (!previousDoc) {
|
||||
throw new Error('previousDoc is missing in afterChange hook')
|
||||
}
|
||||
return validateHookOrder('collectionAfterChange', doc)
|
||||
},
|
||||
],
|
||||
beforeRead: [({ doc }) => validateHookOrder('collectionBeforeRead', doc)],
|
||||
afterRead: [({ doc }) => validateHookOrder('collectionAfterRead', doc)],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'fieldBeforeValidate',
|
||||
@@ -43,7 +29,15 @@ const Hooks: CollectionConfig = {
|
||||
type: 'checkbox',
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
({ data }) => {
|
||||
({ data, operation, previousSiblingDoc, previousValue }) => {
|
||||
if (operation === 'update') {
|
||||
if (typeof previousValue === 'undefined') {
|
||||
throw new Error('previousValue is missing in beforeChange hook')
|
||||
}
|
||||
if (!previousSiblingDoc) {
|
||||
throw new Error('previousSiblingDoc is missing in beforeChange hook')
|
||||
}
|
||||
}
|
||||
data.fieldBeforeChange = true
|
||||
validateHookOrder('fieldBeforeChange', data)
|
||||
return true
|
||||
@@ -104,9 +98,23 @@ const Hooks: CollectionConfig = {
|
||||
type: 'checkbox',
|
||||
},
|
||||
],
|
||||
hooks: {
|
||||
afterChange: [
|
||||
({ doc, previousDoc }) => {
|
||||
if (!previousDoc) {
|
||||
throw new Error('previousDoc is missing in afterChange hook')
|
||||
}
|
||||
return validateHookOrder('collectionAfterChange', doc)
|
||||
},
|
||||
],
|
||||
afterRead: [({ doc }) => validateHookOrder('collectionAfterRead', doc)],
|
||||
beforeChange: [({ data }) => validateHookOrder('collectionBeforeChange', data)],
|
||||
beforeRead: [({ doc }) => validateHookOrder('collectionBeforeRead', doc)],
|
||||
beforeValidate: [({ data }) => validateHookOrder('collectionBeforeValidate', data)],
|
||||
},
|
||||
}
|
||||
|
||||
const createHookOrder = [
|
||||
const writeHooksOrder = [
|
||||
'fieldBeforeValidate',
|
||||
'collectionBeforeValidate',
|
||||
'collectionBeforeChange',
|
||||
@@ -119,10 +127,11 @@ const createHookOrder = [
|
||||
|
||||
const validateHookOrder = (check: string, data) => {
|
||||
let hasMatched
|
||||
createHookOrder.forEach((hook) => {
|
||||
if (check === 'collectionBeforeRead' && !data.id) {
|
||||
data[check] = true
|
||||
} else if (hook === check) {
|
||||
if (check === 'collectionBeforeRead') {
|
||||
data.collectionBeforeRead = true
|
||||
}
|
||||
writeHooksOrder.forEach((hook) => {
|
||||
if (hook === check) {
|
||||
data[check] = true
|
||||
hasMatched = true
|
||||
} else if ((!hasMatched && !data[hook]) || (hasMatched && data[hook])) {
|
||||
|
||||
12
test/hooks/collections/Users/afterLoginHook.ts
Normal file
12
test/hooks/collections/Users/afterLoginHook.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { AfterLoginHook } from '../../../../packages/payload/src/collections/config/types'
|
||||
|
||||
export const afterLoginHook: AfterLoginHook = async ({ req, user }) => {
|
||||
return req.payload.update({
|
||||
id: user.id,
|
||||
collection: 'hooks-users',
|
||||
data: {
|
||||
afterLoginHook: true,
|
||||
},
|
||||
req,
|
||||
})
|
||||
}
|
||||
@@ -6,8 +6,9 @@ import type { Payload } from '../../../../packages/payload/src/payload'
|
||||
|
||||
import { AuthenticationError } from '../../../../packages/payload/src/errors'
|
||||
import { devUser, regularUser } from '../../../credentials'
|
||||
import { afterLoginHook } from './afterLoginHook'
|
||||
|
||||
const beforeLoginHook: BeforeLoginHook = ({ user, req }) => {
|
||||
const beforeLoginHook: BeforeLoginHook = ({ req, user }) => {
|
||||
const isAdmin = user.roles.includes('admin') ? user : undefined
|
||||
if (!isAdmin) {
|
||||
throw new AuthenticationError(req.t)
|
||||
@@ -33,16 +34,21 @@ const Users: CollectionConfig = {
|
||||
fields: [
|
||||
{
|
||||
name: 'roles',
|
||||
label: 'Role',
|
||||
type: 'select',
|
||||
options: ['admin', 'user'],
|
||||
defaultValue: 'user',
|
||||
hasMany: true,
|
||||
label: 'Role',
|
||||
options: ['admin', 'user'],
|
||||
required: true,
|
||||
saveToJWT: true,
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
name: 'afterLoginHook',
|
||||
type: 'checkbox',
|
||||
},
|
||||
],
|
||||
hooks: {
|
||||
afterLogin: [afterLoginHook],
|
||||
beforeLogin: [beforeLoginHook],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('Hooks', () => {
|
||||
beforeAll(async () => {
|
||||
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } })
|
||||
const config = await configPromise
|
||||
client = new RESTClient(config, { serverURL, defaultSlug: transformSlug })
|
||||
client = new RESTClient(config, { defaultSlug: transformSlug, serverURL })
|
||||
apiUrl = `${serverURL}/api`
|
||||
})
|
||||
|
||||
@@ -43,8 +43,8 @@ describe('Hooks', () => {
|
||||
const doc = await payload.create({
|
||||
collection: transformSlug,
|
||||
data: {
|
||||
transform: [2, 8],
|
||||
localizedTransform: [2, 8],
|
||||
transform: [2, 8],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -55,39 +55,75 @@ describe('Hooks', () => {
|
||||
|
||||
describe('hook execution', () => {
|
||||
let doc
|
||||
it('should execute hooks in correct order on create', async () => {
|
||||
const data = {
|
||||
collectionAfterChange: false,
|
||||
collectionAfterRead: false,
|
||||
collectionBeforeChange: false,
|
||||
collectionBeforeRead: false,
|
||||
collectionBeforeValidate: false,
|
||||
fieldAfterChange: false,
|
||||
fieldAfterRead: false,
|
||||
fieldBeforeChange: false,
|
||||
fieldBeforeValidate: false,
|
||||
}
|
||||
beforeEach(async () => {
|
||||
doc = await payload.create({
|
||||
collection: hooksSlug,
|
||||
data: {
|
||||
fieldBeforeValidate: false,
|
||||
collectionBeforeValidate: false,
|
||||
fieldBeforeChange: false,
|
||||
collectionBeforeChange: false,
|
||||
fieldAfterChange: false,
|
||||
collectionAfterChange: false,
|
||||
collectionBeforeRead: false,
|
||||
fieldAfterRead: false,
|
||||
collectionAfterRead: false,
|
||||
},
|
||||
data,
|
||||
})
|
||||
})
|
||||
|
||||
it('should execute hooks in correct order on create', async () => {
|
||||
expect(doc.collectionAfterChange).toBeTruthy()
|
||||
expect(doc.collectionAfterRead).toBeTruthy()
|
||||
expect(doc.collectionBeforeChange).toBeTruthy()
|
||||
// beforeRead is not run on create operation
|
||||
expect(doc.collectionBeforeRead).toBeFalsy()
|
||||
expect(doc.collectionBeforeValidate).toBeTruthy()
|
||||
expect(doc.fieldAfterChange).toBeTruthy()
|
||||
expect(doc.fieldAfterRead).toBeTruthy()
|
||||
expect(doc.fieldBeforeChange).toBeTruthy()
|
||||
expect(doc.fieldBeforeValidate).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should execute hooks in correct order on update', async () => {
|
||||
doc = await payload.update({
|
||||
id: doc.id,
|
||||
collection: hooksSlug,
|
||||
data,
|
||||
})
|
||||
|
||||
expect(doc.fieldBeforeValidate).toEqual(true)
|
||||
expect(doc.collectionBeforeValidate).toEqual(true)
|
||||
expect(doc.fieldBeforeChange).toEqual(true)
|
||||
expect(doc.collectionBeforeChange).toEqual(true)
|
||||
expect(doc.fieldAfterChange).toEqual(true)
|
||||
expect(doc.collectionAfterChange).toEqual(true)
|
||||
expect(doc.fieldAfterRead).toEqual(true)
|
||||
expect(doc.collectionAfterChange).toBeTruthy()
|
||||
expect(doc.collectionAfterRead).toBeTruthy()
|
||||
expect(doc.collectionBeforeChange).toBeTruthy()
|
||||
// beforeRead is not run on update operation
|
||||
expect(doc.collectionBeforeRead).toBeFalsy()
|
||||
expect(doc.collectionBeforeValidate).toBeTruthy()
|
||||
expect(doc.fieldAfterChange).toBeTruthy()
|
||||
expect(doc.fieldAfterRead).toBeTruthy()
|
||||
expect(doc.fieldBeforeChange).toBeTruthy()
|
||||
expect(doc.fieldBeforeValidate).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should execute hooks in correct order on find', async () => {
|
||||
doc = await payload.findByID({
|
||||
id: doc.id,
|
||||
collection: hooksSlug,
|
||||
})
|
||||
|
||||
expect(doc.collectionAfterRead).toBeTruthy()
|
||||
expect(doc.collectionBeforeRead).toBeTruthy()
|
||||
expect(doc.fieldAfterRead).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should save data generated with afterRead hooks in nested field structures', async () => {
|
||||
const document: NestedAfterReadHook = await payload.create({
|
||||
collection: nestedAfterReadHooksSlug,
|
||||
data: {
|
||||
text: 'ok',
|
||||
group: {
|
||||
array: [{ input: 'input' }],
|
||||
},
|
||||
text: 'ok',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -106,7 +142,6 @@ describe('Hooks', () => {
|
||||
const document = await payload.create({
|
||||
collection: nestedAfterReadHooksSlug,
|
||||
data: {
|
||||
text: 'ok',
|
||||
group: {
|
||||
array: [
|
||||
{
|
||||
@@ -117,12 +152,13 @@ describe('Hooks', () => {
|
||||
shouldPopulate: relation.id,
|
||||
},
|
||||
},
|
||||
text: 'ok',
|
||||
},
|
||||
})
|
||||
|
||||
const retrievedDoc = await payload.findByID({
|
||||
collection: nestedAfterReadHooksSlug,
|
||||
id: document.id,
|
||||
collection: nestedAfterReadHooksSlug,
|
||||
})
|
||||
|
||||
expect(retrievedDoc.group.array[0].shouldPopulate.title).toEqual(relation.title)
|
||||
@@ -138,8 +174,8 @@ describe('Hooks', () => {
|
||||
})
|
||||
|
||||
const retrievedDoc = await payload.findByID({
|
||||
collection: chainingHooksSlug,
|
||||
id: document.id,
|
||||
collection: chainingHooksSlug,
|
||||
})
|
||||
|
||||
expect(retrievedDoc.text).toEqual('ok!!')
|
||||
@@ -189,15 +225,15 @@ describe('Hooks', () => {
|
||||
|
||||
const [updatedDoc1, updatedDoc2] = await Promise.all([
|
||||
await payload.update({
|
||||
collection: afterOperationSlug,
|
||||
id: doc1.id,
|
||||
collection: afterOperationSlug,
|
||||
data: {
|
||||
title: 'Title',
|
||||
},
|
||||
}),
|
||||
await payload.update({
|
||||
collection: afterOperationSlug,
|
||||
id: doc2.id,
|
||||
collection: afterOperationSlug,
|
||||
data: {
|
||||
title: 'Title',
|
||||
},
|
||||
@@ -225,8 +261,8 @@ describe('Hooks', () => {
|
||||
})
|
||||
|
||||
const retrievedDoc = await payload.findByID({
|
||||
collection: contextHooksSlug,
|
||||
id: document.id,
|
||||
collection: contextHooksSlug,
|
||||
})
|
||||
|
||||
expect(retrievedDoc.value).toEqual('secret')
|
||||
@@ -235,17 +271,17 @@ describe('Hooks', () => {
|
||||
it('should pass context from local API to hooks', async () => {
|
||||
const document = await payload.create({
|
||||
collection: contextHooksSlug,
|
||||
data: {
|
||||
value: 'wrongvalue',
|
||||
},
|
||||
context: {
|
||||
secretValue: 'data from local API',
|
||||
},
|
||||
data: {
|
||||
value: 'wrongvalue',
|
||||
},
|
||||
})
|
||||
|
||||
const retrievedDoc = await payload.findByID({
|
||||
collection: contextHooksSlug,
|
||||
id: document.id,
|
||||
collection: contextHooksSlug,
|
||||
})
|
||||
|
||||
expect(retrievedDoc.value).toEqual('data from local API')
|
||||
@@ -282,8 +318,8 @@ describe('Hooks', () => {
|
||||
const document = (await response.json()).doc
|
||||
|
||||
const retrievedDoc = await payload.findByID({
|
||||
collection: contextHooksSlug,
|
||||
id: document.id,
|
||||
collection: contextHooksSlug,
|
||||
})
|
||||
|
||||
expect(retrievedDoc.value).toEqual('data from rest API')
|
||||
@@ -291,7 +327,7 @@ describe('Hooks', () => {
|
||||
})
|
||||
|
||||
describe('auth collection hooks', () => {
|
||||
it('allow admin login', async () => {
|
||||
it('should call afterLogin hook', async () => {
|
||||
const { user } = await payload.login({
|
||||
collection: hooksUsersSlug,
|
||||
data: {
|
||||
@@ -299,7 +335,15 @@ describe('Hooks', () => {
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
|
||||
const result = await payload.findByID({
|
||||
id: user.id,
|
||||
collection: hooksUsersSlug,
|
||||
})
|
||||
|
||||
expect(user).toBeDefined()
|
||||
expect(user.afterLoginHook).toStrictEqual(true)
|
||||
expect(result.afterLoginHook).toStrictEqual(true)
|
||||
})
|
||||
|
||||
it('deny user login', async () => {
|
||||
@@ -342,8 +386,8 @@ describe('Hooks', () => {
|
||||
|
||||
// BeforeRead is only run for find operations
|
||||
const foundDoc = await payload.findByID({
|
||||
collection: dataHooksSlug,
|
||||
id: doc.id,
|
||||
collection: dataHooksSlug,
|
||||
})
|
||||
|
||||
expect(JSON.parse(foundDoc.collection_beforeRead_collection)).toStrictEqual(
|
||||
|
||||
2
test/live-preview-vue/.gitignore
vendored
Normal file
2
test/live-preview-vue/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/media
|
||||
/media-gif
|
||||
99
test/live-preview-vue/blocks/ArchiveBlock/index.ts
Normal file
99
test/live-preview-vue/blocks/ArchiveBlock/index.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { Block } from '../../../../packages/payload/src/fields/config/types'
|
||||
|
||||
export const Archive: Block = {
|
||||
slug: 'archive',
|
||||
labels: {
|
||||
singular: 'Archive',
|
||||
plural: 'Archives',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'introContent',
|
||||
label: 'Intro Content',
|
||||
type: 'richText',
|
||||
},
|
||||
{
|
||||
name: 'populateBy',
|
||||
type: 'select',
|
||||
defaultValue: 'collection',
|
||||
options: [
|
||||
{
|
||||
label: 'Collection',
|
||||
value: 'collection',
|
||||
},
|
||||
{
|
||||
label: 'Individual Selection',
|
||||
value: 'selection',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'relationTo',
|
||||
label: 'Collections To Show',
|
||||
defaultValue: 'posts',
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'collection',
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: 'Posts',
|
||||
value: 'posts',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'categories',
|
||||
label: 'Categories To Show',
|
||||
relationTo: 'categories',
|
||||
hasMany: true,
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'collection',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'limit',
|
||||
label: 'Limit',
|
||||
defaultValue: 10,
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'collection',
|
||||
step: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'selectedDocs',
|
||||
label: 'Selection',
|
||||
relationTo: ['posts'],
|
||||
hasMany: true,
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'selection',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'populatedDocs',
|
||||
label: 'Populated Docs',
|
||||
relationTo: ['posts'],
|
||||
hasMany: true,
|
||||
admin: {
|
||||
disabled: true,
|
||||
description: 'This field is auto-populated after-read',
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'collection',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'populatedDocsTotal',
|
||||
label: 'Populated Docs Total',
|
||||
admin: {
|
||||
step: 1,
|
||||
disabled: true,
|
||||
description: 'This field is auto-populated after-read',
|
||||
condition: (_, siblingData) => siblingData.populateBy === 'collection',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
26
test/live-preview-vue/blocks/CallToAction/index.ts
Normal file
26
test/live-preview-vue/blocks/CallToAction/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { Block } from '../../../../packages/payload/src/fields/config/types'
|
||||
|
||||
import { invertBackground } from '../../fields/invertBackground'
|
||||
import linkGroup from '../../fields/linkGroup'
|
||||
|
||||
export const CallToAction: Block = {
|
||||
slug: 'cta',
|
||||
labels: {
|
||||
singular: 'Call to Action',
|
||||
plural: 'Calls to Action',
|
||||
},
|
||||
fields: [
|
||||
invertBackground,
|
||||
{
|
||||
name: 'richText',
|
||||
label: 'Rich Text',
|
||||
type: 'richText',
|
||||
},
|
||||
linkGroup({
|
||||
appearances: ['primary', 'secondary'],
|
||||
overrides: {
|
||||
maxRows: 2,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
58
test/live-preview-vue/blocks/Content/index.ts
Normal file
58
test/live-preview-vue/blocks/Content/index.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { Block, Field } from '../../../../packages/payload/src/fields/config/types'
|
||||
|
||||
import { invertBackground } from '../../fields/invertBackground'
|
||||
import link from '../../fields/link'
|
||||
|
||||
const columnFields: Field[] = [
|
||||
{
|
||||
name: 'size',
|
||||
type: 'select',
|
||||
defaultValue: 'oneThird',
|
||||
options: [
|
||||
{
|
||||
value: 'oneThird',
|
||||
label: 'One Third',
|
||||
},
|
||||
{
|
||||
value: 'half',
|
||||
label: 'Half',
|
||||
},
|
||||
{
|
||||
value: 'twoThirds',
|
||||
label: 'Two Thirds',
|
||||
},
|
||||
{
|
||||
value: 'full',
|
||||
label: 'Full',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'richText',
|
||||
label: 'Rich Text',
|
||||
type: 'richText',
|
||||
},
|
||||
{
|
||||
name: 'enableLink',
|
||||
type: 'checkbox',
|
||||
},
|
||||
link({
|
||||
overrides: {
|
||||
admin: {
|
||||
condition: (_, { enableLink }) => Boolean(enableLink),
|
||||
},
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
export const Content: Block = {
|
||||
slug: 'content',
|
||||
fields: [
|
||||
invertBackground,
|
||||
{
|
||||
name: 'columns',
|
||||
type: 'array',
|
||||
fields: columnFields,
|
||||
},
|
||||
],
|
||||
}
|
||||
31
test/live-preview-vue/blocks/MediaBlock/index.ts
Normal file
31
test/live-preview-vue/blocks/MediaBlock/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { Block } from 'payload/types'
|
||||
|
||||
import { invertBackground } from '../../fields/invertBackground'
|
||||
|
||||
export const MediaBlock: Block = {
|
||||
slug: 'mediaBlock',
|
||||
fields: [
|
||||
invertBackground,
|
||||
{
|
||||
name: 'position',
|
||||
type: 'select',
|
||||
defaultValue: 'default',
|
||||
options: [
|
||||
{
|
||||
label: 'Default',
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
label: 'Fullscreen',
|
||||
value: 'fullscreen',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'media',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
19
test/live-preview-vue/collections/Categories.ts
Normal file
19
test/live-preview-vue/collections/Categories.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types'
|
||||
|
||||
const Categories: CollectionConfig = {
|
||||
slug: 'categories',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default Categories
|
||||
21
test/live-preview-vue/collections/Media.ts
Normal file
21
test/live-preview-vue/collections/Media.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types'
|
||||
|
||||
export const Media: CollectionConfig = {
|
||||
slug: 'media',
|
||||
upload: true,
|
||||
access: {
|
||||
read: () => true,
|
||||
create: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'alt',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'richText',
|
||||
},
|
||||
],
|
||||
}
|
||||
191
test/live-preview-vue/collections/Pages.ts
Normal file
191
test/live-preview-vue/collections/Pages.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types'
|
||||
|
||||
import { lexicalEditor } from '../../../packages/richtext-lexical/src'
|
||||
import { Archive } from '../blocks/ArchiveBlock'
|
||||
import { CallToAction } from '../blocks/CallToAction'
|
||||
import { Content } from '../blocks/Content'
|
||||
import { MediaBlock } from '../blocks/MediaBlock'
|
||||
import CollectionLivePreviewButton from '../components/CollectionLivePreviewButton'
|
||||
import { hero } from '../fields/hero'
|
||||
import { pagesSlug, tenantsSlug } from '../shared'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: pagesSlug,
|
||||
access: {
|
||||
read: () => true,
|
||||
create: () => true,
|
||||
update: () => true,
|
||||
delete: () => true,
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['id', 'title', 'slug', 'createdAt'],
|
||||
components: {
|
||||
views: {
|
||||
Edit: {
|
||||
LivePreview: {
|
||||
actions: [CollectionLivePreviewButton],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
required: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'tenant',
|
||||
type: 'relationship',
|
||||
relationTo: tenantsSlug,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'Hero',
|
||||
fields: [hero],
|
||||
},
|
||||
{
|
||||
label: 'Content',
|
||||
fields: [
|
||||
{
|
||||
name: 'layout',
|
||||
type: 'blocks',
|
||||
blocks: [CallToAction, Content, MediaBlock, Archive],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Test',
|
||||
fields: [
|
||||
{
|
||||
label: 'Rich Text — Slate',
|
||||
type: 'richText',
|
||||
name: 'richTextSlate',
|
||||
},
|
||||
{
|
||||
label: 'Rich Text — Lexical',
|
||||
type: 'richText',
|
||||
name: 'richTextLexical',
|
||||
editor: lexicalEditor({}),
|
||||
},
|
||||
{
|
||||
name: 'relationshipAsUpload',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
},
|
||||
{
|
||||
name: 'relationshipMonoHasOne',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
},
|
||||
{
|
||||
name: 'relationshipMonoHasMany',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
name: 'relationshipPolyHasOne',
|
||||
type: 'relationship',
|
||||
relationTo: ['posts'],
|
||||
},
|
||||
{
|
||||
name: 'relationshipPolyHasMany',
|
||||
type: 'relationship',
|
||||
relationTo: ['posts'],
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
name: 'arrayOfRelationships',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'uploadInArray',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
},
|
||||
{
|
||||
name: 'richTextInArray',
|
||||
type: 'richText',
|
||||
},
|
||||
{
|
||||
name: 'relationshipInArrayMonoHasOne',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
},
|
||||
{
|
||||
name: 'relationshipInArrayMonoHasMany',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
name: 'relationshipInArrayPolyHasOne',
|
||||
type: 'relationship',
|
||||
relationTo: ['posts'],
|
||||
},
|
||||
{
|
||||
name: 'relationshipInArrayPolyHasMany',
|
||||
type: 'relationship',
|
||||
relationTo: ['posts'],
|
||||
hasMany: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Named Tabs',
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
name: 'tab',
|
||||
label: 'Tab',
|
||||
fields: [
|
||||
{
|
||||
name: 'relationshipInTab',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'meta',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
98
test/live-preview-vue/collections/Posts.ts
Normal file
98
test/live-preview-vue/collections/Posts.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types'
|
||||
|
||||
import { Archive } from '../blocks/ArchiveBlock'
|
||||
import { CallToAction } from '../blocks/CallToAction'
|
||||
import { Content } from '../blocks/Content'
|
||||
import { MediaBlock } from '../blocks/MediaBlock'
|
||||
import { hero } from '../fields/hero'
|
||||
import { tenantsSlug } from '../shared'
|
||||
|
||||
export const postsSlug = 'posts'
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
slug: postsSlug,
|
||||
access: {
|
||||
read: () => true,
|
||||
create: () => true,
|
||||
update: () => true,
|
||||
delete: () => true,
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['id', 'title', 'slug', 'createdAt'],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
required: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'tenant',
|
||||
type: 'relationship',
|
||||
relationTo: tenantsSlug,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'Hero',
|
||||
fields: [hero],
|
||||
},
|
||||
{
|
||||
label: 'Content',
|
||||
fields: [
|
||||
{
|
||||
name: 'layout',
|
||||
type: 'blocks',
|
||||
blocks: [CallToAction, Content, MediaBlock, Archive],
|
||||
},
|
||||
{
|
||||
name: 'relatedPosts',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
hasMany: true,
|
||||
filterOptions: ({ id }) => {
|
||||
return {
|
||||
id: {
|
||||
not_in: [id],
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'meta',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
30
test/live-preview-vue/collections/Tenants.ts
Normal file
30
test/live-preview-vue/collections/Tenants.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types'
|
||||
|
||||
import { tenantsSlug } from '../shared'
|
||||
|
||||
export const Tenants: CollectionConfig = {
|
||||
slug: tenantsSlug,
|
||||
access: {
|
||||
read: () => true,
|
||||
create: () => true,
|
||||
update: () => true,
|
||||
delete: () => true,
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['id', 'clientURL'],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'clientURL',
|
||||
label: 'Client URL',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user