Compare commits

..

1 Commits

Author SHA1 Message Date
Dan Ribbens
44231b55cb fix(db-mongodb): error querying with invalid value on date field 2024-01-15 13:48:56 -05:00
203 changed files with 3397 additions and 13213 deletions

View File

@@ -85,7 +85,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
database: [mongoose, postgres, supabase] database: [mongoose, postgres]
env: env:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
@@ -118,22 +118,7 @@ jobs:
postgresql password: ${{ env.POSTGRES_PASSWORD }} postgresql password: ${{ env.POSTGRES_PASSWORD }}
if: matrix.database == 'postgres' if: matrix.database == 'postgres'
- name: Install Supabase CLI - run: sleep 30
uses: supabase/setup-cli@v1
with:
version: latest
if: matrix.database == 'supabase'
- name: Initialize Supabase
run: |
supabase init
supabase start
if: matrix.database == 'supabase'
- name: Wait for PostgreSQL
run: sleep 30
if: matrix.database == 'postgres'
- name: Configure PostgreSQL - name: Configure PostgreSQL
run: | run: |
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE ROLE runner SUPERUSER LOGIN;" psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE ROLE runner SUPERUSER LOGIN;"
@@ -141,11 +126,6 @@ jobs:
echo "POSTGRES_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV echo "POSTGRES_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
if: matrix.database == 'postgres' if: matrix.database == 'postgres'
- name: Configure Supabase
run: |
echo "POSTGRES_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres" >> $GITHUB_ENV
if: matrix.database == 'supabase'
- name: Component Tests - name: Component Tests
run: pnpm test:components run: pnpm test:components

View File

@@ -1,75 +1,3 @@
## [2.10.1](https://github.com/payloadcms/payload/compare/v2.10.0...v2.10.1) (2024-02-09)
### Bug Fixes
* clearable cells handle null values ([#5038](https://github.com/payloadcms/payload/issues/5038)) ([f6d7da7](https://github.com/payloadcms/payload/commit/f6d7da751039df25066b51bb91d6453e1a4efd82))
* **db-mongodb:** handle null values with exists ([#5037](https://github.com/payloadcms/payload/issues/5037)) ([cdc4cb9](https://github.com/payloadcms/payload/commit/cdc4cb971b9180ba2ed09741f5af1a3c18292828))
* **db-postgres:** handle nested docs with drafts ([#5012](https://github.com/payloadcms/payload/issues/5012)) ([da184d4](https://github.com/payloadcms/payload/commit/da184d40ece74bffb224002eb5df8f6987d65043))
* ensures docs with the same id are shown in relationship field select ([#4859](https://github.com/payloadcms/payload/issues/4859)) ([e1813fb](https://github.com/payloadcms/payload/commit/e1813fb884e0dc84203fcbab87527a99a4d3a5d7))
* query relationships by explicit id field ([#5022](https://github.com/payloadcms/payload/issues/5022)) ([a0a58e7](https://github.com/payloadcms/payload/commit/a0a58e7fd20dff54d210c968f4d5defd67441bdd))
* **richtext-lexical:** make editor reactive to initialValue changes ([#5010](https://github.com/payloadcms/payload/issues/5010)) ([2315781](https://github.com/payloadcms/payload/commit/2315781f1891ddde4b4c5f2f0cfa1c17af85b7a9))
## [2.10.0](https://github.com/payloadcms/payload/compare/v2.9.0...v2.10.0) (2024-02-06)
### Features
* add more options to addFieldStatePromise so that it can be used for field flattening ([#4799](https://github.com/payloadcms/payload/issues/4799)) ([8725d41](https://github.com/payloadcms/payload/commit/8725d411645bb0270376e235669f46be2227ecc0))
* extend transactions to cover after and beforeOperation hooks ([#4960](https://github.com/payloadcms/payload/issues/4960)) ([1e8a6b7](https://github.com/payloadcms/payload/commit/1e8a6b7899f7b1e6451cc4d777602208478b483c))
* previousValue and previousSiblingDoc args added to beforeChange field hooks ([#4958](https://github.com/payloadcms/payload/issues/4958)) ([5d934ba](https://github.com/payloadcms/payload/commit/5d934ba02d07d98f781ce983228858ee5ce5c226))
* re-use existing logger instance passed to payload.init ([#3124](https://github.com/payloadcms/payload/issues/3124)) ([471d211](https://github.com/payloadcms/payload/commit/471d2113a790dc0d54b2f8ed84e6899310efd600))
* **richtext-lexical:** Blocks: generate type definitions for blocks fields ([#4529](https://github.com/payloadcms/payload/issues/4529)) ([90d7ee3](https://github.com/payloadcms/payload/commit/90d7ee3e6535d51290fc734b284ff3811dbda1f8))
* use deletion success message from server if provided ([#4966](https://github.com/payloadcms/payload/issues/4966)) ([e3c8105](https://github.com/payloadcms/payload/commit/e3c8105cc2ed6fdf8007d97cd7b5556fc71ed724))
### Bug Fixes
* **db-postgres:** filtering relationships with drafts enabled ([#4998](https://github.com/payloadcms/payload/issues/4998)) ([c3a3942](https://github.com/payloadcms/payload/commit/c3a39429697e9d335e9be199e7caafb82eb26219))
* **db-postgres:** handle schema changes with supabase ([#4968](https://github.com/payloadcms/payload/issues/4968)) ([5d3659d](https://github.com/payloadcms/payload/commit/5d3659d48ad8bbf5d96fbcd80434d2287cab97e0))
* **db-postgres:** indexes not created for non unique field names ([#4967](https://github.com/payloadcms/payload/issues/4967)) ([64f705c](https://github.com/payloadcms/payload/commit/64f705c3c94148972f67e8175e718015760d6430))
* **db-postgres:** indexes not creating for relationships, arrays, hasmany and blocks ([#4976](https://github.com/payloadcms/payload/issues/4976)) ([47106d5](https://github.com/payloadcms/payload/commit/47106d5a1af2ebd073fbbc6e474174c3d3835e5c))
* **db-postgres:** localized field sort count ([#4997](https://github.com/payloadcms/payload/issues/4997)) ([f3876c2](https://github.com/payloadcms/payload/commit/f3876c2a39efe19a1864213306725aadcc14f130))
* ensures docPermissions fallback to collection permissions on create ([#4969](https://github.com/payloadcms/payload/issues/4969)) ([afa2b94](https://github.com/payloadcms/payload/commit/afa2b942e0aad90c55744ae13e0ffe1cefa4585d))
* **migrations:** safely create migration file when no name passed ([#4995](https://github.com/payloadcms/payload/issues/4995)) ([0740d50](https://github.com/payloadcms/payload/commit/0740d5095ee1aef13e4e37f6b174d529f0f2d993))
* **plugin-seo:** tabbedUI with email field causes duplicate field ([#4944](https://github.com/payloadcms/payload/issues/4944)) ([db22cbd](https://github.com/payloadcms/payload/commit/db22cbdf21a39ed0604ab96c57ca4242eac82ce7))
## [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)
### Features
* **db-postgres:** support drizzle logging config ([#4809](https://github.com/payloadcms/payload/issues/4809)) ([371353f](https://github.com/payloadcms/payload/commit/371353f1535fbab4ebd9f56fc14fd10a30eec289))
* **plugin-form-builder:** add validation for form ID when creating a submission ([#4730](https://github.com/payloadcms/payload/pull/4730))
* **plugin-seo:** add support for interfaceName and fieldOverrides ([#4695](https://github.com/payloadcms/payload/pull/4695))
### Bug Fixes
* **db-mongodb:** mongodb versions creating duplicates ([#4825](https://github.com/payloadcms/payload/issues/4825)) ([a861311](https://github.com/payloadcms/payload/commit/a861311c5a98126700f98f9a2ab380782e754717))
* **db-mongodb:** transactionOptions=false typeErrors ([82383a5](https://github.com/payloadcms/payload/commit/82383a5b5f52785115c0feb970da70e91971b7ca))
* **db-postgres:** Remove duplicate keys from response ([#4747](https://github.com/payloadcms/payload/issues/4747)) ([eb9e771](https://github.com/payloadcms/payload/commit/eb9e771a9ca03636486d36654f215b73435574cb))
* **db-postgres:** validateExistingBlockIsIdentical with arrays ([3b88adc](https://github.com/payloadcms/payload/commit/3b88adc7d0594af63ce190c40c9ee3905df67a31))
* **db-postgres:** validateExistingBlockIsIdentical with other tables ([0647c87](https://github.com/payloadcms/payload/commit/0647c870f15dc1b122734b678c2abeb6f56377d4))
* **plugin-seo:** fix missing spread operator in URL generator function ([#4723](https://github.com/payloadcms/payload/pull/4723))
* removes max-width from field-types class & correctly sets it on uploads ([#4829](https://github.com/payloadcms/payload/issues/4829)) ([ee5390a](https://github.com/payloadcms/payload/commit/ee5390aaca37a4154cde8392b60f091ec3e5175c))
## [2.8.1](https://github.com/payloadcms/payload/compare/v2.8.0...v2.8.1) (2024-01-12) ## [2.8.1](https://github.com/payloadcms/payload/compare/v2.8.0...v2.8.1) (2024-01-12)

View File

@@ -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) | | **`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. | | **`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. | | **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`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. | | **`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. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | | **`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. | | **`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) | | **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |

View File

@@ -46,7 +46,6 @@ export const Page: CollectionConfig = {
- [Date](/docs/fields/date) - date / time field that saves a timestamp - [Date](/docs/fields/date) - date / time field that saves a timestamp
- [Email](/docs/fields/email) - validates the entry is a properly formatted email - [Email](/docs/fields/email) - validates the entry is a properly formatted email
- [Group](/docs/fields/group) - nest fields within an object - [Group](/docs/fields/group) - nest fields within an object
- [JSON](/docs/fields/json) - saves actual JSON in the database
- [Number](/docs/fields/number) - field that enforces that its value be a number - [Number](/docs/fields/number) - field that enforces that its value be a number
- [Point](/docs/fields/point) - geometric coordinates for location data - [Point](/docs/fields/point) - geometric coordinates for location data
- [Radio](/docs/fields/radio) - radio button group, allowing only one value to be selected - [Radio](/docs/fields/radio) - radio button group, allowing only one value to be selected

View File

@@ -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. | | **`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. | | **`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) | | **`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 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 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. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | | **`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) | | **`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) | | **`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) |

View File

@@ -75,7 +75,6 @@ import { CollectionBeforeOperationHook } from 'payload/types'
const beforeOperationHook: CollectionBeforeOperationHook = async ({ const beforeOperationHook: CollectionBeforeOperationHook = async ({
args, // original arguments passed into the operation args, // original arguments passed into the operation
operation, // name of the operation operation, // name of the operation
req, // full express request
}) => { }) => {
return args // return modified operation arguments as necessary return args // return modified operation arguments as necessary
} }
@@ -210,7 +209,6 @@ import { CollectionAfterOperationHook } from 'payload/types'
const afterOperationHook: CollectionAfterOperationHook = async ({ const afterOperationHook: CollectionAfterOperationHook = async ({
args, // arguments passed into the operation args, // arguments passed into the operation
operation, // name of the operation operation, // name of the operation
req, // full express request
result, // the result of the operation, before modifications result, // the result of the operation, before modifications
}) => { }) => {
return result // return modified result as necessary return result // return modified result as necessary

View File

@@ -6,8 +6,7 @@ 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 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 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.
functionalities to be easily reusable across your projects.
**Example use cases include:** **Example use cases include:**
@@ -47,8 +46,7 @@ const ExampleField: Field = {
## Arguments and return values ## Arguments and return values
All field-level hooks are formatted to accept the same arguments, although some arguments may be `undefined` based on 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.
which field hook you are utilizing.
<Banner type="success"> <Banner type="success">
<strong>Tip:</strong> <strong>Tip:</strong>
@@ -71,10 +69,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. | | **`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. | | **`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. | | **`previousDoc`** | The document before changes were applied, only in `afterChange` hooks. |
| **`previousSiblingDoc`** | The sibling data of the document before changes being applied, only in `beforeChange` and `afterChange` hook. | | **`previousSiblingDoc`** | The sibling data from the previous document in `afterChange` hook. |
| **`req`** | The Express `request` object. It is mocked for Local API operations. | | **`req`** | The Express `request` object. It is mocked for Local API operations. |
| **`value`** | The value of the field. | | **`value`** | The value of the field. |
| **`previousValue`** | The previous value of the field, before changes, only in `beforeChange` and `afterChange` hooks. | | **`previousValue`** | The previous value of the field, before changes were applied, only in `afterChange` hooks. |
| **`context`** | Context passed to this hook. More info can be found under [Context](/docs/hooks/context) | | **`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. | | **`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. | | **`collection`** | The collection which the field belongs to. If the field belongs to a global, this will be null. |
@@ -82,8 +80,7 @@ Field Hooks receive one `args` argument that contains the following properties:
#### Return value #### Return value
All field hooks can optionally modify the return value of the field before the operation continues. Field Hooks may 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.
optionally return the value that should be used within the field.
<Banner type="warning"> <Banner type="warning">
<strong>Important</strong> <strong>Important</strong>
@@ -95,14 +92,11 @@ optionally return the value that should be used within the field.
## Examples of Field Hooks ## Examples of Field Hooks
To better illustrate how field-level hooks can be applied, here are some specific examples. These demonstrate the 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.
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 ### beforeValidate
Runs before the `update` operation. This hook allows you to pre-process or format field data before it undergoes Runs before the `update` operation. This hook allows you to pre-process or format field data before it undergoes validation.
validation.
```ts ```ts
import { Field } from 'payload/types' import { Field } from 'payload/types'
@@ -119,15 +113,11 @@ const usernameField: Field = {
} }
``` ```
In this example, the `beforeValidate` hook is used to process the `username` field. The hook takes the incoming value of 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.
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 ### beforeChange
Immediately following validation, `beforeChange` hooks will run within `create` and `update` operations. At this stage, 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.
you can be confident that the field data that will be saved to the document is valid in accordance to your field
validations.
```ts ```ts
import { Field } from 'payload/types' import { Field } from 'payload/types'
@@ -146,14 +136,11 @@ const emailField: Field = {
} }
``` ```
In the `emailField`, the `beforeChange` hook checks the `operation` type. If the operation is `create`, it performs 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.
additional validation or transformation on the email field value. This allows for operation-specific logic to be applied
to the field.
### afterChange ### afterChange
The `afterChange` hook is executed after a field's value has been changed and saved in the database. This hook is useful 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.
for post-processing or triggering side effects based on the new value of the field.
```ts ```ts
import { Field } from 'payload/types' import { Field } from 'payload/types'
@@ -178,15 +165,11 @@ const membershipStatusField: Field = {
} }
``` ```
In this example, the `afterChange` hook is used with a `membershipStatusField`, which allows users to select their 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.
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 ### afterRead
The `afterRead` hook is invoked after a field value is read from the database. This is ideal for formatting or 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.
transforming the field data for output.
```ts ```ts
import { Field } from 'payload/types' import { Field } from 'payload/types'
@@ -203,9 +186,8 @@ const dateField: Field = {
} }
``` ```
Here, the `afterRead` hook for the `dateField` is used to format the date into a more readable format 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.
using `toLocaleDateString()`. This hook modifies the way the date is presented to the user, making it more
user-friendly.
## TypeScript ## TypeScript

View File

@@ -159,39 +159,6 @@ A function called by the search preview component to display the actual URL of y
} }
``` ```
#### `interfaceName`
Rename the meta group interface name that is generated for TypeScript and GraphQL.
```ts
// payload.config.ts
{
// ...
seoPlugin({
interfaceName: 'customInterfaceNameSEO'
})
}
```
#### `fieldOverrides`
Pass any valid field props to the base fields: Title, Description or Image.
```ts
// payload.config.ts
seoPlugin({
// ...
fieldOverrides: {
title: {
required: true,
},
description: {
localized: true,
},
},
})
```
## TypeScript ## TypeScript
All types can be directly imported: All types can be directly imported:

View File

@@ -98,13 +98,6 @@ On boot, a seed script is included to scaffold a basic database for you to use a
> NOTICE: seeding the database is destructive because it drops your current database to populate a fresh one from the seed template. Only run this command if you are starting a new project or can afford to lose your current data. > NOTICE: seeding the database is destructive because it drops your current database to populate a fresh one from the seed template. Only run this command if you are starting a new project or can afford to lose your current data.
### Conflicting routes
>In a monorepo when routes are bootstrapped to the same host, they can conflict with Payload's own routes if they have the same name. In our template we've named the Nextjs API routes to `next` to avoid this conflict.
>
>This can happen with any other routes conflicting with Payload such as `admin` and we recommend using different names for custom routes.
>Alternatively you can also rename Payload's own routes via the [configuration](https://payloadcms.com/docs/configuration/overview).
## Production ## Production
To run Payload in production, you need to build and serve the Admin panel. To do so, follow these steps: To run Payload in production, you need to build and serve the Admin panel. To do so, follow these steps:

View File

@@ -0,0 +1,5 @@
import { NextResponse } from 'next/server'
export async function GET(): Promise<NextResponse> {
return NextResponse.json({ success: true })
}

View File

@@ -0,0 +1,5 @@
import { NextResponse } from 'next/server'
export async function POST(): Promise<NextResponse> {
return NextResponse.json({ success: true })
}

View File

@@ -1,10 +0,0 @@
import { NextResponse } from 'next/server'
/**
* The Next.js API routes can conflict with Payload's own routes if they share the same path
* To avoid this you can customise the path of Payload or the API route of Nextjs as we've done here
* See readme: https://github.com/payloadcms/payload/tree/main/examples/custom-server#conflicting-routes
* */
export async function GET(): Promise<NextResponse> {
return NextResponse.json({ success: true })
}

View File

@@ -1,10 +0,0 @@
import { NextResponse } from 'next/server'
/**
* The Next.js API routes can conflict with Payload's own routes if they share the same path
* To avoid this you can customise the path of Payload or the API route of Nextjs as we've done here
* See readme: https://github.com/payloadcms/payload/tree/main/examples/custom-server#conflicting-routes
* */
export async function POST(): Promise<NextResponse> {
return NextResponse.json({ success: true })
}

View File

@@ -1,2 +0,0 @@
DATABASE_URI=mongodb://127.0.0.1/payload-template-blank
PAYLOAD_SECRET=YOUR_SECRET_HERE

View File

@@ -1,6 +0,0 @@
build
dist
/media
node_modules
.DS_Store
.env

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: 'typescript',
semi: false,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
}

View File

@@ -1,58 +0,0 @@
# 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).

View File

@@ -1,6 +0,0 @@
{
"$schema": "https://json.schemastore.org/nodemon.json",
"ext": "ts",
"exec": "ts-node src/server.ts -- -I",
"stdin": false
}

View File

@@ -1,35 +0,0 @@
{
"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"
}
}

View File

@@ -1,79 +0,0 @@
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',
},
],
}

View File

@@ -1,32 +0,0 @@
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,
},
],
},
],
}

View File

@@ -1,15 +0,0 @@
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

View File

@@ -1,30 +0,0 @@
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,
}),
})

View File

@@ -1,27 +0,0 @@
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()

View File

@@ -1,22 +0,0 @@
{
"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
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,27 +2,26 @@ import type { AfterLoginHook } from 'payload/dist/collections/config/types'
export const recordLastLoggedInTenant: AfterLoginHook = async ({ req, user }) => { export const recordLastLoggedInTenant: AfterLoginHook = async ({ req, user }) => {
try { try {
const relatedOrg = await req.payload const relatedOrg = await req.payload.find({
.find({ collection: 'tenants',
collection: 'tenants', where: {
where: { 'domains.domain': {
'domains.domain': { in: [req.headers.host],
in: [req.headers.host],
},
}, },
depth: 0,
limit: 1,
})
?.then(res => res.docs?.[0])
await req.payload.update({
id: user.id,
collection: 'users',
data: {
lastLoggedInTenant: relatedOrg?.id || null,
}, },
req, depth: 0,
limit: 1,
}) })
if (relatedOrg.docs.length > 0) {
await req.payload.update({
id: user.id,
collection: 'users',
data: {
lastLoggedInTenant: relatedOrg.docs[0].id,
},
})
}
} catch (err: unknown) { } catch (err: unknown) {
req.payload.logger.error(`Error recording last logged in tenant for user ${user.id}: ${err}`) req.payload.logger.error(`Error recording last logged in tenant for user ${user.id}: ${err}`)
} }

View File

@@ -30,7 +30,6 @@ export const isSuperOrTenantAdmin = async (args: { req: PayloadRequest }): Promi
}, },
depth: 0, depth: 0,
limit: 1, limit: 1,
req,
}) })
// if this tenant does not exist, deny access // if this tenant does not exist, deny access

File diff suppressed because it is too large Load Diff

View File

@@ -64,7 +64,7 @@
"copyfiles": "2.4.1", "copyfiles": "2.4.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"dotenv": "8.6.0", "dotenv": "8.6.0",
"drizzle-orm": "0.29.3", "drizzle-orm": "0.28.5",
"express": "4.18.2", "express": "4.18.2",
"form-data": "3.0.1", "form-data": "3.0.1",
"fs-extra": "10.1.0", "fs-extra": "10.1.0",
@@ -94,7 +94,7 @@
"slash": "3.0.0", "slash": "3.0.0",
"slate": "0.91.4", "slate": "0.91.4",
"tempfile": "^3.0.0", "tempfile": "^3.0.0",
"ts-node": "10.9.2", "ts-node": "10.9.1",
"turbo": "^1.11.1", "turbo": "^1.11.1",
"typescript": "5.2.2", "typescript": "5.2.2",
"uuid": "^9.0.1" "uuid": "^9.0.1"
@@ -104,16 +104,6 @@
"react-i18next": "11.18.6", "react-i18next": "11.18.6",
"react-router-dom": "5.3.4" "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": { "engines": {
"node": ">=14", "node": ">=14",
"pnpm": ">=8" "pnpm": ">=8"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@payloadcms/db-mongodb", "name": "@payloadcms/db-mongodb",
"version": "1.4.1", "version": "1.3.1",
"description": "The officially supported MongoDB database adapter for Payload", "description": "The officially supported MongoDB database adapter for Payload",
"repository": "https://github.com/payloadcms/payload", "repository": "https://github.com/payloadcms/payload",
"license": "MIT", "license": "MIT",

View File

@@ -29,18 +29,15 @@ export const connect: Connect = async function connect(this: MongooseAdapter, pa
urlToConnect = process.env.PAYLOAD_TEST_MONGO_URL urlToConnect = process.env.PAYLOAD_TEST_MONGO_URL
} else { } else {
connectionOptions.dbName = 'payloadmemory' connectionOptions.dbName = 'payloadmemory'
const { MongoMemoryReplSet } = require('mongodb-memory-server') const { MongoMemoryServer } = require('mongodb-memory-server')
const getPort = require('get-port') const getPort = require('get-port')
const port = await getPort() const port = await getPort()
this.mongoMemoryServer = await MongoMemoryReplSet.create({ this.mongoMemoryServer = await MongoMemoryServer.create({
instance: { instance: {
dbName: 'payloadmemory', dbName: 'payloadmemory',
port, port,
}, },
replSet: {
count: 3,
},
}) })
urlToConnect = this.mongoMemoryServer.getUri() urlToConnect = this.mongoMemoryServer.getUri()
@@ -53,7 +50,7 @@ export const connect: Connect = async function connect(this: MongooseAdapter, pa
const client = this.connection.getClient() const client = this.connection.getClient()
if (!client.options.replicaSet) { if (!client.options.replicaSet || this.transactionOptions === false) {
this.transactionOptions = false this.transactionOptions = false
this.beginTransaction = undefined this.beginTransaction = undefined
} }

View File

@@ -49,7 +49,6 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
], ],
}, },
{ $unset: { latest: 1 } }, { $unset: { latest: 1 } },
options,
) )
const result: Document = JSON.parse(JSON.stringify(doc)) const result: Document = JSON.parse(JSON.stringify(doc))

View File

@@ -32,7 +32,7 @@ export const createMigration: CreateMigration = async function createMigration({
// Check for predefined migration. // Check for predefined migration.
// Either passed in via --file or prefixed with @payloadcms/db-mongodb/ // Either passed in via --file or prefixed with @payloadcms/db-mongodb/
if (file || migrationName?.startsWith('@payloadcms/db-mongodb/')) { if (file || migrationName.startsWith('@payloadcms/db-mongodb/')) {
if (!file) file = migrationName if (!file) file = migrationName
const predefinedMigrationName = file.replace('@payloadcms/db-mongodb/', '') const predefinedMigrationName = file.replace('@payloadcms/db-mongodb/', '')
@@ -59,8 +59,8 @@ export const createMigration: CreateMigration = async function createMigration({
const timestamp = `${formattedDate}_${formattedTime}` const timestamp = `${formattedDate}_${formattedTime}`
const formattedName = migrationName?.replace(/\W/g, '_') const formattedName = migrationName.replace(/\W/g, '_')
const fileName = migrationName ? `${timestamp}_${formattedName}.ts` : `${timestamp}_migration.ts` const fileName = `${timestamp}_${formattedName}.ts`
const filePath = `${dir}/${fileName}` const filePath = `${dir}/${fileName}`
fs.writeFileSync(filePath, migrationFileContent) fs.writeFileSync(filePath, migrationFileContent)
payload.logger.info({ msg: `Migration created at ${filePath}` }) payload.logger.info({ msg: `Migration created at ${filePath}` })

View File

@@ -57,7 +57,6 @@ export const createVersion: CreateVersion = async function createVersion(
], ],
}, },
{ $unset: { latest: 1 } }, { $unset: { latest: 1 } },
options,
) )
const result: Document = JSON.parse(JSON.stringify(doc)) const result: Document = JSON.parse(JSON.stringify(doc))

View File

@@ -63,7 +63,6 @@ export const find: Find = async function find(
paginationOptions.useCustomCountFn = () => { paginationOptions.useCustomCountFn = () => {
return Promise.resolve( return Promise.resolve(
Model.countDocuments(query, { Model.countDocuments(query, {
...options,
hint: { _id: 1 }, hint: { _id: 1 },
}), }),
) )

View File

@@ -82,7 +82,6 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
paginationOptions.useCustomCountFn = () => { paginationOptions.useCustomCountFn = () => {
return Promise.resolve( return Promise.resolve(
Model.countDocuments(query, { Model.countDocuments(query, {
...options,
hint: { _id: 1 }, hint: { _id: 1 },
}), }),
) )

View File

@@ -79,7 +79,6 @@ export const findVersions: FindVersions = async function findVersions(
paginationOptions.useCustomCountFn = () => { paginationOptions.useCustomCountFn = () => {
return Promise.resolve( return Promise.resolve(
Model.countDocuments(query, { Model.countDocuments(query, {
...options,
hint: { _id: 1 }, hint: { _id: 1 },
}), }),
) )

View File

@@ -93,13 +93,18 @@ export function mongooseAdapter({
connectOptions, connectOptions,
disableIndexHints = false, disableIndexHints = false,
migrationDir: migrationDirArg, migrationDir: migrationDirArg,
transactionOptions = {}, transactionOptions,
url, url,
}: Args): MongooseAdapterResult { }: Args): MongooseAdapterResult {
function adapter({ payload }: { payload: Payload }) { function adapter({ payload }: { payload: Payload }) {
const migrationDir = findMigrationDir(migrationDirArg) const migrationDir = findMigrationDir(migrationDirArg)
let beginTransactionFunction = beginTransaction
mongoose.set('strictQuery', false) mongoose.set('strictQuery', false)
if (transactionOptions === false) {
beginTransactionFunction = () => null
}
return createDatabaseAdapter<MongooseAdapter>({ return createDatabaseAdapter<MongooseAdapter>({
name: 'mongoose', name: 'mongoose',
@@ -117,7 +122,7 @@ export function mongooseAdapter({
versions: {}, versions: {},
// DatabaseAdapter // DatabaseAdapter
beginTransaction: transactionOptions ? beginTransaction : undefined, beginTransaction: beginTransactionFunction,
commitTransaction, commitTransaction,
connect, connect,
create, create,

View File

@@ -11,30 +11,25 @@ import type { MongooseAdapter } from '.'
/** /**
* Drop the current database and run all migrate up functions * Drop the current database and run all migrate up functions
*/ */
export async function migrateFresh( export async function migrateFresh(this: MongooseAdapter): Promise<void> {
this: MongooseAdapter,
{ forceAcceptWarning = false }: { forceAcceptWarning?: boolean },
): Promise<void> {
const { payload } = this const { payload } = this
if (!forceAcceptWarning) { const { confirm: acceptWarning } = await prompts(
const { confirm: acceptWarning } = await prompts( {
{ name: 'confirm',
name: 'confirm', type: 'confirm',
type: 'confirm', initial: false,
initial: false, message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`,
message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`, },
{
onCancel: () => {
process.exit(0)
}, },
{ },
onCancel: () => { )
process.exit(0)
},
},
)
if (!acceptWarning) { if (!acceptWarning) {
process.exit(0) process.exit(0)
}
} }
payload.logger.info({ payload.logger.info({

View File

@@ -65,7 +65,8 @@ export const sanitizeQueryValue = ({
if (field.type === 'date' && typeof val === 'string') { if (field.type === 'date' && typeof val === 'string') {
formattedValue = new Date(val) formattedValue = new Date(val)
if (Number.isNaN(Date.parse(formattedValue))) { if (Number.isNaN(Date.parse(formattedValue))) {
return undefined // ignore invalid query
return { rawQuery: {} }
} }
} }
@@ -77,7 +78,6 @@ export const sanitizeQueryValue = ({
// Object equality requires the value to be the first key in the object that is being queried. // Object equality requires the value to be the first key in the object that is being queried.
if ( if (
operator === 'equals' && operator === 'equals' &&
formattedValue &&
typeof formattedValue === 'object' && typeof formattedValue === 'object' &&
formattedValue.value && formattedValue.value &&
formattedValue.relationTo formattedValue.relationTo
@@ -157,23 +157,6 @@ export const sanitizeQueryValue = ({
if (operator === 'exists') { if (operator === 'exists') {
formattedValue = formattedValue === 'true' || formattedValue === true formattedValue = formattedValue === 'true' || formattedValue === true
// Clearable fields
if (['relationship', 'select', 'upload'].includes(field.type)) {
if (formattedValue) {
return {
rawQuery: {
$and: [{ [path]: { $exists: true } }, { [path]: { $ne: null } }],
},
}
} else {
return {
rawQuery: {
$or: [{ [path]: { $exists: false } }, { [path]: { $eq: null } }],
},
}
}
}
} }
return { operator: formattedOperator, val: formattedValue } return { operator: formattedOperator, val: formattedValue }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@payloadcms/db-postgres", "name": "@payloadcms/db-postgres",
"version": "0.5.2", "version": "0.3.1",
"description": "The officially supported Postgres database adapter for Payload", "description": "The officially supported Postgres database adapter for Payload",
"repository": "https://github.com/payloadcms/payload", "repository": "https://github.com/payloadcms/payload",
"license": "MIT", "license": "MIT",
@@ -22,8 +22,8 @@
"dependencies": { "dependencies": {
"@libsql/client": "^0.3.1", "@libsql/client": "^0.3.1",
"console-table-printer": "2.11.2", "console-table-printer": "2.11.2",
"drizzle-kit": "0.20.14-1f2c838", "drizzle-kit": "0.19.13-e99bac1",
"drizzle-orm": "0.29.3", "drizzle-orm": "0.28.5",
"pg": "8.11.3", "pg": "8.11.3",
"prompts": "2.4.2", "prompts": "2.4.2",
"to-snake-case": "1.0.0", "to-snake-case": "1.0.0",

View File

@@ -18,9 +18,8 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
try { try {
this.pool = new Pool(this.poolOptions) this.pool = new Pool(this.poolOptions)
await this.pool.connect() await this.pool.connect()
const logger = this.logger || false
this.drizzle = drizzle(this.pool, { schema: this.schema, logger }) this.drizzle = drizzle(this.pool, { schema: this.schema })
if (process.env.PAYLOAD_DROP_DATABASE === 'true') { if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
this.payload.logger.info('---- DROPPING TABLES ----') this.payload.logger.info('---- DROPPING TABLES ----')
await this.drizzle.execute(sql`drop schema public cascade; await this.drizzle.execute(sql`drop schema public cascade;
@@ -40,7 +39,7 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
) )
return return
const { pushSchema } = require('drizzle-kit/payload') const { pushSchema } = require('drizzle-kit/utils')
// This will prompt if clarifications are needed for Drizzle to push new schema // This will prompt if clarifications are needed for Drizzle to push new schema
const { apply, hasDataLoss, statementsToExecute, warnings } = await pushSchema( const { apply, hasDataLoss, statementsToExecute, warnings } = await pushSchema(
@@ -60,9 +59,9 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
const { confirm: acceptWarnings } = await prompts( const { confirm: acceptWarnings } = await prompts(
{ {
name: 'confirm', name: 'confirm',
type: 'confirm',
initial: false, initial: false,
message, message,
type: 'confirm',
}, },
{ {
onCancel: () => { onCancel: () => {

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */ /* eslint-disable no-restricted-syntax, no-await-in-loop */
import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload' import type { DrizzleSnapshotJSON } from 'drizzle-kit/utils'
import type { CreateMigration } from 'payload/database' import type { CreateMigration } from 'payload/database'
import fs from 'fs' import fs from 'fs'
@@ -53,14 +53,14 @@ const getDefaultDrizzleSnapshot = (): DrizzleSnapshotJSON => ({
export const createMigration: CreateMigration = async function createMigration( export const createMigration: CreateMigration = async function createMigration(
this: PostgresAdapter, this: PostgresAdapter,
{ forceAcceptWarning, migrationName, payload }, { migrationName, payload },
) { ) {
const dir = payload.db.migrationDir const dir = payload.db.migrationDir
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
fs.mkdirSync(dir) fs.mkdirSync(dir)
} }
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/payload') const { generateDrizzleJson, generateMigration } = require('drizzle-kit/utils')
const [yyymmdd, hhmmss] = new Date().toISOString().split('T') const [yyymmdd, hhmmss] = new Date().toISOString().split('T')
const formattedDate = yyymmdd.replace(/\D/g, '') const formattedDate = yyymmdd.replace(/\D/g, '')
@@ -95,13 +95,13 @@ export const createMigration: CreateMigration = async function createMigration(
const sqlStatementsUp = await generateMigration(drizzleJsonBefore, drizzleJsonAfter) const sqlStatementsUp = await generateMigration(drizzleJsonBefore, drizzleJsonAfter)
const sqlStatementsDown = await generateMigration(drizzleJsonAfter, drizzleJsonBefore) const sqlStatementsDown = await generateMigration(drizzleJsonAfter, drizzleJsonBefore)
if (!sqlStatementsUp.length && !sqlStatementsDown.length && !forceAcceptWarning) { if (!sqlStatementsUp.length && !sqlStatementsDown.length) {
const { confirm: shouldCreateBlankMigration } = await prompts( const { confirm: shouldCreateBlankMigration } = await prompts(
{ {
name: 'confirm', name: 'confirm',
type: 'confirm',
initial: false, initial: false,
message: 'No schema changes detected. Would you like to create a blank migration file?', message: 'No schema changes detected. Would you like to create a blank migration file?',
type: 'confirm',
}, },
{ {
onCancel: () => { onCancel: () => {

View File

@@ -158,7 +158,7 @@ export const findMany = async function find({
query: db query: db
.select({ .select({
count: sql<number>`count count: sql<number>`count
(DISTINCT ${adapter.tables[tableName].id})`, (*)`,
}) })
.from(table) .from(table)
.where(where), .where(where),

View File

@@ -50,7 +50,6 @@ export function postgresAdapter(args: Args): PostgresAdapterResult {
drizzle: undefined, drizzle: undefined,
enums: {}, enums: {},
fieldConstraints: {}, fieldConstraints: {},
logger: args.logger,
pool: undefined, pool: undefined,
poolOptions: args.pool, poolOptions: args.pool,
push: args.push, push: args.push,

View File

@@ -80,7 +80,7 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
} }
async function runMigrationFile(payload: Payload, migration: Migration, batch: number) { async function runMigrationFile(payload: Payload, migration: Migration, batch: number) {
const { generateDrizzleJson } = require('drizzle-kit/payload') const { generateDrizzleJson } = require('drizzle-kit/utils')
const start = Date.now() const start = Date.now()
const req = { payload } as PayloadRequest const req = { payload } as PayloadRequest

View File

@@ -37,7 +37,7 @@ export async function migrateDown(this: PostgresAdapter): Promise<void> {
} }
const start = Date.now() const start = Date.now()
const req = { payload } as PayloadRequest const req = {} as PayloadRequest
try { try {
payload.logger.info({ msg: `Migrating down: ${migrationFile.name}` }) payload.logger.info({ msg: `Migrating down: ${migrationFile.name}` })

View File

@@ -14,30 +14,25 @@ import { parseError } from './utilities/parseError'
/** /**
* Drop the current database and run all migrate up functions * Drop the current database and run all migrate up functions
*/ */
export async function migrateFresh( export async function migrateFresh(this: PostgresAdapter): Promise<void> {
this: PostgresAdapter,
{ forceAcceptWarning = false },
): Promise<void> {
const { payload } = this const { payload } = this
if (forceAcceptWarning === false) { const { confirm: acceptWarning } = await prompts(
const { confirm: acceptWarning } = await prompts( {
{ name: 'confirm',
name: 'confirm', type: 'confirm',
type: 'confirm', initial: false,
initial: false, message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`,
message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`, },
{
onCancel: () => {
process.exit(0)
}, },
{ },
onCancel: () => { )
process.exit(0)
},
},
)
if (!acceptWarning) { if (!acceptWarning) {
process.exit(0) process.exit(0)
}
} }
payload.logger.info({ payload.logger.info({

View File

@@ -2,7 +2,7 @@
import type { SQL } from 'drizzle-orm' import type { SQL } from 'drizzle-orm'
import type { Field, FieldAffectingData, TabAsField } from 'payload/types' import type { Field, FieldAffectingData, TabAsField } from 'payload/types'
import { and, eq, like, sql } from 'drizzle-orm' import { and, eq, sql } from 'drizzle-orm'
import { alias } from 'drizzle-orm/pg-core' import { alias } from 'drizzle-orm/pg-core'
import { APIError } from 'payload/errors' import { APIError } from 'payload/errors'
import { fieldAffectsData, tabHasName } from 'payload/types' import { fieldAffectsData, tabHasName } from 'payload/types'
@@ -44,10 +44,6 @@ type Args = {
rootTableName?: string rootTableName?: string
selectFields: Record<string, GenericColumn> selectFields: Record<string, GenericColumn>
tableName: string tableName: string
/**
* If creating a new table name for arrays and blocks, this suffix should be appended to the table name
*/
tableNameSuffix?: string
} }
/** /**
* Transforms path to table and column name * Transforms path to table and column name
@@ -69,7 +65,6 @@ export const getTableColumnFromPath = ({
rootTableName: incomingRootTableName, rootTableName: incomingRootTableName,
selectFields, selectFields,
tableName, tableName,
tableNameSuffix = '',
}: Args): TableColumn => { }: Args): TableColumn => {
const fieldPath = incomingSegments[0] const fieldPath = incomingSegments[0]
let locale = incomingLocale let locale = incomingLocale
@@ -130,7 +125,6 @@ export const getTableColumnFromPath = ({
rootTableName, rootTableName,
selectFields, selectFields,
tableName: newTableName, tableName: newTableName,
tableNameSuffix,
}) })
} }
case 'tab': { case 'tab': {
@@ -140,7 +134,7 @@ export const getTableColumnFromPath = ({
aliasTable, aliasTable,
collectionPath, collectionPath,
columnPrefix: `${columnPrefix}${field.name}_`, columnPrefix: `${columnPrefix}${field.name}_`,
constraintPath: `${constraintPath}${field.name}.`, constraintPath,
constraints, constraints,
fields: field.fields, fields: field.fields,
joinAliases, joinAliases,
@@ -150,7 +144,6 @@ export const getTableColumnFromPath = ({
rootTableName, rootTableName,
selectFields, selectFields,
tableName: newTableName, tableName: newTableName,
tableNameSuffix: `${tableNameSuffix}${toSnakeCase(field.name)}_`,
}) })
} }
return getTableColumnFromPath({ return getTableColumnFromPath({
@@ -168,7 +161,6 @@ export const getTableColumnFromPath = ({
rootTableName, rootTableName,
selectFields, selectFields,
tableName: newTableName, tableName: newTableName,
tableNameSuffix,
}) })
} }
@@ -193,7 +185,7 @@ export const getTableColumnFromPath = ({
aliasTable, aliasTable,
collectionPath, collectionPath,
columnPrefix: `${columnPrefix}${field.name}_`, columnPrefix: `${columnPrefix}${field.name}_`,
constraintPath: `${constraintPath}${field.name}.`, constraintPath,
constraints, constraints,
fields: field.fields, fields: field.fields,
joinAliases, joinAliases,
@@ -203,12 +195,11 @@ export const getTableColumnFromPath = ({
rootTableName, rootTableName,
selectFields, selectFields,
tableName: newTableName, tableName: newTableName,
tableNameSuffix: `${tableNameSuffix}${toSnakeCase(field.name)}_`,
}) })
} }
case 'array': { case 'array': {
newTableName = `${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}` newTableName = `${tableName}_${toSnakeCase(field.name)}`
constraintPath = `${constraintPath}${field.name}.%.` constraintPath = `${constraintPath}${field.name}.%.`
if (locale && field.localized && adapter.payload.config.localization) { if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and( joins[newTableName] = and(
@@ -326,15 +317,21 @@ export const getTableColumnFromPath = ({
// Join in the relationships table // Join in the relationships table
joinAliases.push({ joinAliases.push({
condition: and( condition: eq(
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent), (aliasTable || adapter.tables[rootTableName]).id,
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`), aliasRelationshipTable.parent,
), ),
table: aliasRelationshipTable, table: aliasRelationshipTable,
}) })
selectFields[`${relationTableName}.path`] = aliasRelationshipTable.path selectFields[`${relationTableName}.path`] = aliasRelationshipTable.path
constraints.push({
columnName: 'path',
table: aliasRelationshipTable,
value: `${constraintPath}${field.name}`,
})
let newAliasTable let newAliasTable
if (typeof field.relationTo === 'string') { if (typeof field.relationTo === 'string') {
@@ -349,7 +346,7 @@ export const getTableColumnFromPath = ({
table: newAliasTable, table: newAliasTable,
}) })
if (newCollectionPath === '' || newCollectionPath === 'id') { if (newCollectionPath === '') {
return { return {
columnName: `${field.relationTo}ID`, columnName: `${field.relationTo}ID`,
constraints, constraints,
@@ -431,7 +428,7 @@ export const getTableColumnFromPath = ({
columnName: `${columnPrefix}${field.name}`, columnName: `${columnPrefix}${field.name}`,
constraints, constraints,
field, field,
pathSegments, pathSegments: pathSegments,
table: targetTable, table: targetTable,
} }
} }

View File

@@ -207,16 +207,6 @@ export async function parseParams({
break break
} }
if (operator === 'equals' && queryValue === null) {
constraints.push(isNull(rawColumn || table[columnName]))
break
}
if (operator === 'not_equals' && queryValue === null) {
constraints.push(isNotNull(rawColumn || table[columnName]))
break
}
constraints.push( constraints.push(
operatorMap[queryOperator](rawColumn || table[columnName], queryValue), operatorMap[queryOperator](rawColumn || table[columnName], queryValue),
) )

View File

@@ -0,0 +1,46 @@
// type GenerateMigration = (before: DrizzleSnapshotJSON, after: DrizzleSnapshotJSON) => string[]
// type GenerateDrizzleJSON = (schema: DrizzleSchemaExports) => DrizzleSnapshotJSON
// type PushDiff = (schema: DrizzleSchemaExports) => Promise<{ warnings: string[], apply: () => Promise<void> }>
// drizzle-kit@utils
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
async function generateUsage() {
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/utils')
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
const schema = await import('./data/users')
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
const schemaAfter = await import('./data/users-after')
const drizzleJsonBefore = generateDrizzleJson(schema)
const drizzleJsonAfter = generateDrizzleJson(schemaAfter)
const sqlStatements = await generateMigration(drizzleJsonBefore, drizzleJsonAfter)
console.log(sqlStatements)
}
async function pushUsage() {
const { pushSchema } = require('drizzle-kit/utils')
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
const schemaAfter = await import('./data/users-after')
const db = drizzle(new Pool({ connectionString: '' }))
const response = await pushSchema(schemaAfter, db)
console.log('\n')
console.log('hasDataLoss: ', response.hasDataLoss)
console.log('warnings: ', response.warnings)
console.log('statements: ', response.statementsToExecute)
await response.apply()
process.exit(0)
}

View File

@@ -27,9 +27,9 @@ type Args = {
adapter: PostgresAdapter adapter: PostgresAdapter
baseColumns?: Record<string, PgColumnBuilder> baseColumns?: Record<string, PgColumnBuilder>
baseExtraConfig?: Record<string, (cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder> baseExtraConfig?: Record<string, (cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder>
buildTexts?: boolean
buildNumbers?: boolean buildNumbers?: boolean
buildRelationships?: boolean buildRelationships?: boolean
buildTexts?: boolean
disableNotNull: boolean disableNotNull: boolean
disableUnique: boolean disableUnique: boolean
fields: Field[] fields: Field[]
@@ -42,8 +42,8 @@ type Args = {
} }
type Result = { type Result = {
hasManyNumberField: 'index' | boolean
hasManyTextField: 'index' | boolean hasManyTextField: 'index' | boolean
hasManyNumberField: 'index' | boolean
relationsToBuild: Map<string, string> relationsToBuild: Map<string, string>
} }
@@ -51,9 +51,9 @@ export const buildTable = ({
adapter, adapter,
baseColumns = {}, baseColumns = {},
baseExtraConfig = {}, baseExtraConfig = {},
buildTexts,
buildNumbers, buildNumbers,
buildRelationships, buildRelationships,
buildTexts,
disableNotNull, disableNotNull,
disableUnique = false, disableUnique = false,
fields, fields,
@@ -100,16 +100,16 @@ export const buildTable = ({
columns.id = idColTypeMap[idColType]('id').primaryKey() columns.id = idColTypeMap[idColType]('id').primaryKey()
;({ ;({
hasLocalizedField, hasLocalizedField,
hasLocalizedManyNumberField,
hasLocalizedManyTextField, hasLocalizedManyTextField,
hasLocalizedManyNumberField,
hasLocalizedRelationshipField, hasLocalizedRelationshipField,
hasManyNumberField,
hasManyTextField, hasManyTextField,
hasManyNumberField,
} = traverseFields({ } = traverseFields({
adapter, adapter,
buildTexts,
buildNumbers, buildNumbers,
buildRelationships, buildRelationships,
buildTexts,
columns, columns,
disableNotNull, disableNotNull,
disableUnique, disableUnique,
@@ -196,12 +196,12 @@ export const buildTable = ({
const textsTableName = `${rootTableName}_texts` const textsTableName = `${rootTableName}_texts`
const columns: Record<string, PgColumnBuilder> = { const columns: Record<string, PgColumnBuilder> = {
id: serial('id').primaryKey(), id: serial('id').primaryKey(),
text: varchar('text'),
order: integer('order').notNull(), order: integer('order').notNull(),
parent: parentIDColumnMap[idColType]('parent_id') parent: parentIDColumnMap[idColType]('parent_id')
.references(() => table.id, { onDelete: 'cascade' }) .references(() => table.id, { onDelete: 'cascade' })
.notNull(), .notNull(),
path: varchar('path').notNull(), path: varchar('path').notNull(),
text: varchar('text'),
} }
if (hasLocalizedManyTextField) { if (hasLocalizedManyTextField) {
@@ -210,15 +210,15 @@ export const buildTable = ({
textsTable = pgTable(textsTableName, columns, (cols) => { textsTable = pgTable(textsTableName, columns, (cols) => {
const indexes: Record<string, IndexBuilder> = { const indexes: Record<string, IndexBuilder> = {
orderParentIdx: index(`${textsTableName}_order_parent_idx`).on(cols.order, cols.parent), orderParentIdx: index('order_parent_idx').on(cols.order, cols.parent),
} }
if (hasManyTextField === 'index') { if (hasManyTextField === 'index') {
indexes.text_idx = index(`${textsTableName}_text_idx`).on(cols.text) indexes.text_idx = index('text_idx').on(cols.text)
} }
if (hasLocalizedManyTextField) { if (hasLocalizedManyTextField) {
indexes.localeParent = index(`${textsTableName}_locale_parent`).on(cols.locale, cols.parent) indexes.localeParent = index('locale_parent').on(cols.locale, cols.parent)
} }
return indexes return indexes
@@ -254,18 +254,15 @@ export const buildTable = ({
numbersTable = pgTable(numbersTableName, columns, (cols) => { numbersTable = pgTable(numbersTableName, columns, (cols) => {
const indexes: Record<string, IndexBuilder> = { const indexes: Record<string, IndexBuilder> = {
orderParentIdx: index(`${numbersTableName}_order_parent_idx`).on(cols.order, cols.parent), orderParentIdx: index('order_parent_idx').on(cols.order, cols.parent),
} }
if (hasManyNumberField === 'index') { if (hasManyNumberField === 'index') {
indexes.numberIdx = index(`${numbersTableName}_number_idx`).on(cols.number) indexes.numberIdx = index('number_idx').on(cols.number)
} }
if (hasLocalizedManyNumberField) { if (hasLocalizedManyNumberField) {
indexes.localeParent = index(`${numbersTableName}_locale_parent`).on( indexes.localeParent = index('locale_parent').on(cols.locale, cols.parent)
cols.locale,
cols.parent,
)
} }
return indexes return indexes
@@ -316,13 +313,13 @@ export const buildTable = ({
relationshipsTable = pgTable(relationshipsTableName, relationshipColumns, (cols) => { relationshipsTable = pgTable(relationshipsTableName, relationshipColumns, (cols) => {
const result: Record<string, unknown> = { const result: Record<string, unknown> = {
order: index(`${relationshipsTableName}_order_idx`).on(cols.order), order: index('order_idx').on(cols.order),
parentIdx: index(`${relationshipsTableName}_parent_idx`).on(cols.parent), parentIdx: index('parent_idx').on(cols.parent),
pathIdx: index(`${relationshipsTableName}_path_idx`).on(cols.path), pathIdx: index('path_idx').on(cols.path),
} }
if (hasLocalizedRelationshipField) { if (hasLocalizedRelationshipField) {
result.localeIdx = index(`${relationshipsTableName}_locale_idx`).on(cols.locale) result.localeIdx = index('locale_idx').on(cols.locale)
} }
return result return result
@@ -384,5 +381,5 @@ export const buildTable = ({
adapter.relations[`relations_${tableName}`] = tableRelations adapter.relations[`relations_${tableName}`] = tableRelations
return { hasManyNumberField, hasManyTextField, relationsToBuild } return { hasManyTextField, hasManyNumberField, relationsToBuild }
} }

View File

@@ -6,11 +6,10 @@ import type { GenericColumn } from '../types'
type CreateIndexArgs = { type CreateIndexArgs = {
columnName: string columnName: string
name: string | string[] name: string | string[]
tableName: string
unique?: boolean unique?: boolean
} }
export const createIndex = ({ name, columnName, tableName, unique }: CreateIndexArgs) => { export const createIndex = ({ name, columnName, unique }: CreateIndexArgs) => {
return (table: { [x: string]: GenericColumn }) => { return (table: { [x: string]: GenericColumn }) => {
let columns let columns
if (Array.isArray(name)) { if (Array.isArray(name)) {
@@ -21,8 +20,7 @@ export const createIndex = ({ name, columnName, tableName, unique }: CreateIndex
} else { } else {
columns = [table[name]] columns = [table[name]]
} }
if (unique) if (unique) return uniqueIndex(`${columnName}_idx`).on(columns[0], ...columns.slice(1))
return uniqueIndex(`${tableName}_${columnName}_idx`).on(columns[0], ...columns.slice(1)) return index(`${columnName}_idx`).on(columns[0], ...columns.slice(1))
return index(`${tableName}_${columnName}_idx`).on(columns[0], ...columns.slice(1))
} }
} }

View File

@@ -32,9 +32,9 @@ import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdent
type Args = { type Args = {
adapter: PostgresAdapter adapter: PostgresAdapter
buildTexts: boolean
buildNumbers: boolean buildNumbers: boolean
buildRelationships: boolean buildRelationships: boolean
buildTexts: boolean
columnPrefix?: string columnPrefix?: string
columns: Record<string, PgColumnBuilder> columns: Record<string, PgColumnBuilder>
disableNotNull: boolean disableNotNull: boolean
@@ -56,18 +56,18 @@ type Args = {
type Result = { type Result = {
hasLocalizedField: boolean hasLocalizedField: boolean
hasLocalizedManyNumberField: boolean
hasLocalizedManyTextField: boolean hasLocalizedManyTextField: boolean
hasLocalizedManyNumberField: boolean
hasLocalizedRelationshipField: boolean hasLocalizedRelationshipField: boolean
hasManyNumberField: 'index' | boolean
hasManyTextField: 'index' | boolean hasManyTextField: 'index' | boolean
hasManyNumberField: 'index' | boolean
} }
export const traverseFields = ({ export const traverseFields = ({
adapter, adapter,
buildTexts,
buildNumbers, buildNumbers,
buildRelationships, buildRelationships,
buildTexts,
columnPrefix, columnPrefix,
columns, columns,
disableNotNull, disableNotNull,
@@ -122,7 +122,7 @@ export const traverseFields = ({
if ( if (
(field.unique || field.index) && (field.unique || field.index) &&
!['array', 'blocks', 'group', 'point', 'relationship', 'upload'].includes(field.type) && !['array', 'blocks', 'group', 'point', 'relationship', 'upload'].includes(field.type) &&
!('hasMany' in field && field.hasMany === true) !(field.type === 'number' && field.hasMany === true)
) { ) {
const unique = disableUnique !== true && field.unique const unique = disableUnique !== true && field.unique
if (unique) { if (unique) {
@@ -132,10 +132,9 @@ export const traverseFields = ({
} }
adapter.fieldConstraints[rootTableName][`${columnName}_idx`] = constraintValue adapter.fieldConstraints[rootTableName][`${columnName}_idx`] = constraintValue
} }
targetIndexes[`${newTableName}_${field.name}Idx`] = createIndex({ targetIndexes[`${field.name}Idx`] = createIndex({
name: fieldName, name: fieldName,
columnName, columnName,
tableName: newTableName,
unique, unique,
}) })
} }
@@ -242,18 +241,17 @@ export const traverseFields = ({
string, string,
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder (cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
> = { > = {
orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order), orderIdx: (cols) => index('order_idx').on(cols.order),
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent), parentIdx: (cols) => index('parent_idx').on(cols.parent),
} }
if (field.localized) { if (field.localized) {
baseColumns.locale = adapter.enums.enum__locales('locale').notNull() baseColumns.locale = adapter.enums.enum__locales('locale').notNull()
baseExtraConfig.localeIdx = (cols) => baseExtraConfig.localeIdx = (cols) => index('locale_idx').on(cols.locale)
index(`${selectTableName}_locale_idx`).on(cols.locale)
} }
if (field.index) { if (field.index) {
baseExtraConfig.value = (cols) => index(`${selectTableName}_value_idx`).on(cols.value) baseExtraConfig.value = (cols) => index('value_idx').on(cols.value)
} }
buildTable({ buildTable({
@@ -306,19 +304,18 @@ export const traverseFields = ({
string, string,
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder (cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
> = { > = {
_orderIdx: (cols) => index(`${arrayTableName}_order_idx`).on(cols._order), _orderIdx: (cols) => index('_order_idx').on(cols._order),
_parentIDIdx: (cols) => index(`${arrayTableName}_parent_id_idx`).on(cols._parentID), _parentIDIdx: (cols) => index('_parent_id_idx').on(cols._parentID),
} }
if (field.localized && adapter.payload.config.localization) { if (field.localized && adapter.payload.config.localization) {
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull() baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
baseExtraConfig._localeIdx = (cols) => baseExtraConfig._localeIdx = (cols) => index('_locale_idx').on(cols._locale)
index(`${arrayTableName}_locale_idx`).on(cols._locale)
} }
const { const {
hasManyNumberField: subHasManyNumberField,
hasManyTextField: subHasManyTextField, hasManyTextField: subHasManyTextField,
hasManyNumberField: subHasManyNumberField,
relationsToBuild: subRelationsToBuild, relationsToBuild: subRelationsToBuild,
} = buildTable({ } = buildTable({
adapter, adapter,
@@ -387,20 +384,19 @@ export const traverseFields = ({
string, string,
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder (cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
> = { > = {
_orderIdx: (cols) => index(`${blockTableName}_order_idx`).on(cols._order), _orderIdx: (cols) => index('order_idx').on(cols._order),
_parentIDIdx: (cols) => index(`${blockTableName}_parent_id_idx`).on(cols._parentID), _parentIDIdx: (cols) => index('parent_id_idx').on(cols._parentID),
_pathIdx: (cols) => index(`${blockTableName}_path_idx`).on(cols._path), _pathIdx: (cols) => index('path_idx').on(cols._path),
} }
if (field.localized && adapter.payload.config.localization) { if (field.localized && adapter.payload.config.localization) {
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull() baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
baseExtraConfig._localeIdx = (cols) => baseExtraConfig._localeIdx = (cols) => index('locale_idx').on(cols._locale)
index(`${blockTableName}_locale_idx`).on(cols._locale)
} }
const { const {
hasManyNumberField: subHasManyNumberField,
hasManyTextField: subHasManyTextField, hasManyTextField: subHasManyTextField,
hasManyNumberField: subHasManyNumberField,
relationsToBuild: subRelationsToBuild, relationsToBuild: subRelationsToBuild,
} = buildTable({ } = buildTable({
adapter, adapter,
@@ -469,16 +465,16 @@ export const traverseFields = ({
if (!('name' in field)) { if (!('name' in field)) {
const { const {
hasLocalizedField: groupHasLocalizedField, hasLocalizedField: groupHasLocalizedField,
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
hasLocalizedManyTextField: groupHasLocalizedManyTextField, hasLocalizedManyTextField: groupHasLocalizedManyTextField,
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
hasLocalizedRelationshipField: groupHasLocalizedRelationshipField, hasLocalizedRelationshipField: groupHasLocalizedRelationshipField,
hasManyNumberField: groupHasManyNumberField,
hasManyTextField: groupHasManyTextField, hasManyTextField: groupHasManyTextField,
hasManyNumberField: groupHasManyNumberField,
} = traverseFields({ } = traverseFields({
adapter, adapter,
buildTexts,
buildNumbers, buildNumbers,
buildRelationships, buildRelationships,
buildTexts,
columnPrefix, columnPrefix,
columns, columns,
disableNotNull, disableNotNull,
@@ -511,16 +507,16 @@ export const traverseFields = ({
const { const {
hasLocalizedField: groupHasLocalizedField, hasLocalizedField: groupHasLocalizedField,
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
hasLocalizedManyTextField: groupHasLocalizedManyTextField, hasLocalizedManyTextField: groupHasLocalizedManyTextField,
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
hasLocalizedRelationshipField: groupHasLocalizedRelationshipField, hasLocalizedRelationshipField: groupHasLocalizedRelationshipField,
hasManyNumberField: groupHasManyNumberField,
hasManyTextField: groupHasManyTextField, hasManyTextField: groupHasManyTextField,
hasManyNumberField: groupHasManyNumberField,
} = traverseFields({ } = traverseFields({
adapter, adapter,
buildTexts,
buildNumbers, buildNumbers,
buildRelationships, buildRelationships,
buildTexts,
columnPrefix: `${columnName}_`, columnPrefix: `${columnName}_`,
columns, columns,
disableNotNull: disableNotNullFromHere, disableNotNull: disableNotNullFromHere,
@@ -554,16 +550,16 @@ export const traverseFields = ({
const { const {
hasLocalizedField: tabHasLocalizedField, hasLocalizedField: tabHasLocalizedField,
hasLocalizedManyNumberField: tabHasLocalizedManyNumberField,
hasLocalizedManyTextField: tabHasLocalizedManyTextField, hasLocalizedManyTextField: tabHasLocalizedManyTextField,
hasLocalizedManyNumberField: tabHasLocalizedManyNumberField,
hasLocalizedRelationshipField: tabHasLocalizedRelationshipField, hasLocalizedRelationshipField: tabHasLocalizedRelationshipField,
hasManyNumberField: tabHasManyNumberField,
hasManyTextField: tabHasManyTextField, hasManyTextField: tabHasManyTextField,
hasManyNumberField: tabHasManyNumberField,
} = traverseFields({ } = traverseFields({
adapter, adapter,
buildTexts,
buildNumbers, buildNumbers,
buildRelationships, buildRelationships,
buildTexts,
columnPrefix, columnPrefix,
columns, columns,
disableNotNull: disableNotNullFromHere, disableNotNull: disableNotNullFromHere,
@@ -597,16 +593,16 @@ export const traverseFields = ({
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const { const {
hasLocalizedField: rowHasLocalizedField, hasLocalizedField: rowHasLocalizedField,
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField,
hasLocalizedManyTextField: rowHasLocalizedManyTextField, hasLocalizedManyTextField: rowHasLocalizedManyTextField,
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField,
hasLocalizedRelationshipField: rowHasLocalizedRelationshipField, hasLocalizedRelationshipField: rowHasLocalizedRelationshipField,
hasManyNumberField: rowHasManyNumberField,
hasManyTextField: rowHasManyTextField, hasManyTextField: rowHasManyTextField,
hasManyNumberField: rowHasManyNumberField,
} = traverseFields({ } = traverseFields({
adapter, adapter,
buildTexts,
buildNumbers, buildNumbers,
buildRelationships, buildRelationships,
buildTexts,
columnPrefix, columnPrefix,
columns, columns,
disableNotNull: disableNotNullFromHere, disableNotNull: disableNotNullFromHere,
@@ -667,10 +663,10 @@ export const traverseFields = ({
return { return {
hasLocalizedField, hasLocalizedField,
hasLocalizedManyNumberField,
hasLocalizedManyTextField, hasLocalizedManyTextField,
hasLocalizedManyNumberField,
hasLocalizedRelationshipField, hasLocalizedRelationshipField,
hasManyNumberField,
hasManyTextField, hasManyTextField,
hasManyNumberField,
} }
} }

View File

@@ -16,10 +16,7 @@ const getFlattenedFieldNames = (fields: Field[], prefix: string = ''): string[]
return fields.reduce((fieldsToUse, field) => { return fields.reduce((fieldsToUse, field) => {
let fieldPrefix = prefix let fieldPrefix = prefix
if ( if (field.type === 'blocks') {
['array', 'blocks', 'relationship', 'upload'].includes(field.type) ||
('hasMany' in field && field.hasMany === true)
) {
return fieldsToUse return fieldsToUse
} }
@@ -57,27 +54,29 @@ export const validateExistingBlockIsIdentical = ({
rootTableName, rootTableName,
table, table,
}: Args): void => { }: Args): void => {
const fieldNames = getFlattenedFieldNames(block.fields) if (table) {
const fieldNames = getFlattenedFieldNames(block.fields)
const missingField = const missingField =
// ensure every field from the config is in the matching table // ensure every field from the config is in the matching table
fieldNames.find((name) => Object.keys(table).indexOf(name) === -1) || fieldNames.find((name) => Object.keys(table).indexOf(name) === -1) ||
// ensure every table column is matched for every field from the config // ensure every table column is matched for every field from the config
Object.keys(table).find((fieldName) => { Object.keys(table).find((fieldName) => {
if (!['_locale', '_order', '_parentID', '_path', '_uuid'].includes(fieldName)) { if (!['_locale', '_order', '_parentID', '_path', '_uuid'].includes(fieldName)) {
return fieldNames.indexOf(fieldName) === -1 return fieldNames.indexOf(fieldName) === -1
} }
}) })
if (missingField) { if (missingField) {
throw new InvalidConfiguration( throw new InvalidConfiguration(
`The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One block includes the field ${missingField}, while the other block does not.`, `The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One block includes the field ${missingField}, while the other block does not.`,
) )
} }
if (Boolean(localized) !== Boolean(table._locale)) { if (Boolean(localized) !== Boolean(table._locale)) {
throw new InvalidConfiguration( throw new InvalidConfiguration(
`The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One is localized, but another is not. Block schemas of the same name must match exactly.`, `The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One is localized, but another is not. Block schemas of the same name must match exactly.`,
) )
}
} }
} }

View File

@@ -7,8 +7,8 @@ import { fieldAffectsData } from 'payload/types'
import type { BlocksMap } from '../../utilities/createBlocksMap' import type { BlocksMap } from '../../utilities/createBlocksMap'
import { transformHasManyNumber } from './hasManyNumber' import { transformHasManyNumber } from './hasManyNumber'
import { transformHasManyText } from './hasManyText'
import { transformRelationship } from './relationship' import { transformRelationship } from './relationship'
import { transformHasManyText } from './hasManyText'
type TraverseFieldsArgs = { type TraverseFieldsArgs = {
/** /**
@@ -35,6 +35,10 @@ type TraverseFieldsArgs = {
* An array of Payload fields to traverse * An array of Payload fields to traverse
*/ */
fields: (Field | TabAsField)[] fields: (Field | TabAsField)[]
/**
* All hasMany text fields, as returned by Drizzle, keyed on an object by field path
*/
texts: Record<string, Record<string, unknown>[]>
/** /**
* All hasMany number fields, as returned by Drizzle, keyed on an object by field path * All hasMany number fields, as returned by Drizzle, keyed on an object by field path
*/ */
@@ -51,10 +55,6 @@ type TraverseFieldsArgs = {
* Data structure representing the nearest table from db * Data structure representing the nearest table from db
*/ */
table: Record<string, unknown> table: Record<string, unknown>
/**
* All hasMany text fields, as returned by Drizzle, keyed on an object by field path
*/
texts: Record<string, Record<string, unknown>[]>
} }
// Traverse fields recursively, transforming data // Traverse fields recursively, transforming data
@@ -66,11 +66,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix, fieldPrefix,
fields, fields,
texts,
numbers, numbers,
path, path,
relationships, relationships,
table, table,
texts,
}: TraverseFieldsArgs): T => { }: TraverseFieldsArgs): T => {
const sanitizedPath = path ? `${path}.` : path const sanitizedPath = path ? `${path}.` : path
@@ -83,11 +83,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix, fieldPrefix,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
texts,
numbers, numbers,
path, path,
relationships, relationships,
table, table,
texts,
}) })
} }
@@ -103,22 +103,17 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix, fieldPrefix,
fields: field.fields, fields: field.fields,
texts,
numbers, numbers,
path, path,
relationships, relationships,
table, table,
texts,
}) })
} }
if (fieldAffectsData(field)) { if (fieldAffectsData(field)) {
const fieldName = `${fieldPrefix || ''}${field.name}` const fieldName = `${fieldPrefix || ''}${field.name}`
const fieldData = table[fieldName] const fieldData = table[fieldName]
if (fieldPrefix) {
deletions.push(() => delete table[fieldName])
}
if (field.type === 'array') { if (field.type === 'array') {
if (Array.isArray(fieldData)) { if (Array.isArray(fieldData)) {
if (field.localized) { if (field.localized) {
@@ -140,17 +135,13 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix: '', fieldPrefix: '',
fields: field.fields, fields: field.fields,
texts,
numbers, numbers,
path: `${sanitizedPath}${field.name}.${row._order - 1}`, path: `${sanitizedPath}${field.name}.${row._order - 1}`,
relationships, relationships,
table: row, table: row,
texts,
}) })
if ('_order' in rowResult) {
delete rowResult._order
}
arrayResult[locale].push(rowResult) arrayResult[locale].push(rowResult)
} }
@@ -162,11 +153,6 @@ export const traverseFields = <T extends Record<string, unknown>>({
row.id = row._uuid row.id = row._uuid
delete row._uuid delete row._uuid
} }
if ('_order' in row) {
delete row._order
}
return traverseFields<T>({ return traverseFields<T>({
blocks, blocks,
config, config,
@@ -174,11 +160,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix: '', fieldPrefix: '',
fields: field.fields, fields: field.fields,
texts,
numbers, numbers,
path: `${sanitizedPath}${field.name}.${i}`, path: `${sanitizedPath}${field.name}.${i}`,
relationships, relationships,
table: row, table: row,
texts,
}) })
}) })
} }
@@ -218,11 +204,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix: '', fieldPrefix: '',
fields: block.fields, fields: block.fields,
texts,
numbers, numbers,
path: `${blockFieldPath}.${row._order - 1}`, path: `${blockFieldPath}.${row._order - 1}`,
relationships, relationships,
table: row, table: row,
texts,
}) })
delete blockResult._order delete blockResult._order
@@ -249,11 +235,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix: '', fieldPrefix: '',
fields: block.fields, fields: block.fields,
texts,
numbers, numbers,
path: `${blockFieldPath}.${i}`, path: `${blockFieldPath}.${i}`,
relationships, relationships,
table: row, table: row,
texts,
}) })
} }
@@ -330,15 +316,15 @@ export const traverseFields = <T extends Record<string, unknown>>({
transformHasManyText({ transformHasManyText({
field, field,
locale, locale,
ref: result,
textRows: texts, textRows: texts,
ref: result,
}) })
}) })
} else { } else {
transformHasManyText({ transformHasManyText({
field, field,
ref: result,
textRows: textPathMatch, textRows: textPathMatch,
ref: result,
}) })
} }
@@ -434,16 +420,13 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix: groupFieldPrefix, fieldPrefix: groupFieldPrefix,
fields: field.fields, fields: field.fields,
texts,
numbers, numbers,
path: `${sanitizedPath}${field.name}`, path: `${sanitizedPath}${field.name}`,
relationships, relationships,
table, table,
texts,
}) })
}) })
if ('_order' in ref) {
delete ref._order
}
} else { } else {
const groupData = {} const groupData = {}
@@ -454,15 +437,12 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix: groupFieldPrefix, fieldPrefix: groupFieldPrefix,
fields: field.fields, fields: field.fields,
texts,
numbers, numbers,
path: `${sanitizedPath}${field.name}`, path: `${sanitizedPath}${field.name}`,
relationships, relationships,
table, table,
texts,
}) })
if ('_order' in ref) {
delete ref._order
}
} }
break break

View File

@@ -1,7 +1,6 @@
import type { import type {
ColumnBaseConfig, ColumnBaseConfig,
ColumnDataType, ColumnDataType,
DrizzleConfig,
ExtractTablesWithRelations, ExtractTablesWithRelations,
Relation, Relation,
Relations, Relations,
@@ -10,13 +9,11 @@ import type { NodePgDatabase, NodePgQueryResultHKT } from 'drizzle-orm/node-post
import type { PgColumn, PgEnum, PgTableWithColumns, PgTransaction } from 'drizzle-orm/pg-core' import type { PgColumn, PgEnum, PgTableWithColumns, PgTransaction } from 'drizzle-orm/pg-core'
import type { Payload } from 'payload' import type { Payload } from 'payload'
import type { BaseDatabaseAdapter } from 'payload/database' import type { BaseDatabaseAdapter } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { Pool, PoolConfig } from 'pg' import type { Pool, PoolConfig } from 'pg'
export type DrizzleDB = NodePgDatabase<Record<string, unknown>> export type DrizzleDB = NodePgDatabase<Record<string, unknown>>
export type Args = { export type Args = {
logger?: DrizzleConfig['logger']
migrationDir?: string migrationDir?: string
pool: PoolConfig pool: PoolConfig
push?: boolean push?: boolean
@@ -51,12 +48,6 @@ export type DrizzleTransaction = PgTransaction<
export type PostgresAdapter = BaseDatabaseAdapter & { export type PostgresAdapter = BaseDatabaseAdapter & {
drizzle: DrizzleDB drizzle: DrizzleDB
enums: Record<string, GenericEnum> 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 pool: Pool
poolOptions: Args['pool'] poolOptions: Args['pool']
push: boolean push: boolean
@@ -70,12 +61,17 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
} }
} }
tables: Record<string, GenericTable> 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 PostgresAdapterResult = (args: { payload: Payload }) => PostgresAdapter
export type MigrateUpArgs = { payload: Payload; req?: Partial<PayloadRequest> } export type MigrateUpArgs = { payload: Payload }
export type MigrateDownArgs = { payload: Payload; req?: Partial<PayloadRequest> } export type MigrateDownArgs = { payload: Payload }
declare module 'payload' { declare module 'payload' {
export interface DatabaseAdapter export interface DatabaseAdapter
@@ -83,7 +79,6 @@ declare module 'payload' {
BaseDatabaseAdapter { BaseDatabaseAdapter {
drizzle: DrizzleDB drizzle: DrizzleDB
enums: Record<string, GenericEnum> enums: Record<string, GenericEnum>
fieldConstraints: Record<string, Record<string, string>>
pool: Pool pool: Pool
push: boolean push: boolean
relations: Record<string, GenericRelation> relations: Record<string, GenericRelation>
@@ -96,5 +91,6 @@ declare module 'payload' {
} }
} }
tables: Record<string, GenericTable> tables: Record<string, GenericTable>
fieldConstraints: Record<string, Record<string, string>>
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "payload", "name": "payload",
"version": "2.10.1", "version": "2.8.1",
"description": "Node, React and MongoDB Headless CMS and Application Framework", "description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT", "license": "MIT",
"main": "./dist/index.js", "main": "./dist/index.js",
@@ -59,7 +59,7 @@
"@faceless-ui/scroll-info": "1.3.0", "@faceless-ui/scroll-info": "1.3.0",
"@faceless-ui/window-info": "2.1.1", "@faceless-ui/window-info": "2.1.1",
"@monaco-editor/react": "4.5.1", "@monaco-editor/react": "4.5.1",
"@swc/core": "1.3.107", "@swc/core": "1.3.76",
"@swc/register": "0.1.10", "@swc/register": "0.1.10",
"body-parser": "1.20.2", "body-parser": "1.20.2",
"body-scroll-lock": "4.0.0-beta.0", "body-scroll-lock": "4.0.0-beta.0",

View File

@@ -64,7 +64,7 @@ const DeleteDocument: React.FC<Props> = (props) => {
if (res.status < 400) { if (res.status < 400) {
setDeleting(false) setDeleting(false)
toggleModal(modalSlug) toggleModal(modalSlug)
toast.success(json.message || t('titleDeleted', { label: getTranslation(singular, i18n), title })) toast.success(t('titleDeleted', { label: getTranslation(singular, i18n), title }))
return history.push(`${admin}/collections/${slug}`) return history.push(`${admin}/collections/${slug}`)
} }

View File

@@ -33,7 +33,7 @@ export const DocumentControls: React.FC<{
id?: string id?: string
isAccountView?: boolean isAccountView?: boolean
isEditing?: boolean isEditing?: boolean
permissions?: CollectionPermission | GlobalPermission permissions?: CollectionPermission | GlobalPermission | null
}> = (props) => { }> = (props) => {
const { const {
id, id,

View File

@@ -20,7 +20,7 @@ export const getCustomViews = (args: {
? collection?.admin?.components?.views?.Edit ? collection?.admin?.components?.views?.Edit
: undefined : undefined
const defaultViewKeys = Object.keys(defaultCollectionViews()) const defaultViewKeys = Object.keys(defaultCollectionViews)
customViews = Object.entries(collectionViewsConfig || {}).reduce((prev, [key, view]) => { customViews = Object.entries(collectionViewsConfig || {}).reduce((prev, [key, view]) => {
if (defaultViewKeys.includes(key)) { if (defaultViewKeys.includes(key)) {
@@ -38,7 +38,7 @@ export const getCustomViews = (args: {
? global?.admin?.components?.views?.Edit ? global?.admin?.components?.views?.Edit
: undefined : undefined
const defaultViewKeys = Object.keys(defaultGlobalViews()) const defaultViewKeys = Object.keys(defaultGlobalViews)
customViews = Object.entries(globalViewsConfig || {}).reduce((prev, [key, view]) => { customViews = Object.entries(globalViewsConfig || {}).reduce((prev, [key, view]) => {
if (defaultViewKeys.includes(key)) { if (defaultViewKeys.includes(key)) {

View File

@@ -133,10 +133,9 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
const moreThanOneAvailableCollection = enabledCollectionConfigs.length > 1 const moreThanOneAvailableCollection = enabledCollectionConfigs.length > 1
useEffect(() => { useEffect(() => {
const { slug, admin: { listSearchableFields } = {}, versions } = selectedCollectionConfig const { slug, admin: { listSearchableFields } = {} } = selectedCollectionConfig
const params: { const params: {
cacheBust?: number cacheBust?: number
draft?: string
limit?: number limit?: number
page?: number page?: number
search?: string search?: string
@@ -173,7 +172,6 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
if (sort) params.sort = sort if (sort) params.sort = sort
if (cacheBust) params.cacheBust = cacheBust if (cacheBust) params.cacheBust = cacheBust
if (copyOfWhere) params.where = copyOfWhere if (copyOfWhere) params.where = copyOfWhere
if (versions?.drafts) params.draft = 'true'
setParams(params) setParams(params)
}, [ }, [

View File

@@ -12,83 +12,42 @@ import { fieldAffectsData, fieldHasSubFields, tabHasName } from '../../../../../
import getValueWithDefault from '../../../../../fields/getDefaultValue' import getValueWithDefault from '../../../../../fields/getDefaultValue'
import { iterateFields } from './iterateFields' import { iterateFields } from './iterateFields'
export type AddFieldStatePromiseArgs = { type Args = {
/**
* if all parents are localized, then the field is localized
*/
anyParentLocalized?: boolean
config: SanitizedConfig config: SanitizedConfig
data: Data data: Data
field: NonPresentationalField 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 fullData: Data
id: number | string id: number | string
/**
* Whether the field schema should be included in the state
*/
includeSchema?: boolean
locale: string locale: string
/**
* Whether to omit parent fields in the state. @default false
*/
omitParents?: boolean
operation: 'create' | 'update' operation: 'create' | 'update'
passesCondition: boolean passesCondition: boolean
path: string path: string
preferences: { preferences: {
[key: string]: unknown [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 state: Fields
t: TFunction t: TFunction
user: User user: User
} }
/** export const addFieldStatePromise = async ({
* Flattens the fields schema and fields data. id,
* The output is the field path (e.g. array.0.name) mapped to a FormField object. config,
*/ data,
export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Promise<void> => { field,
const { fullData,
id, locale,
anyParentLocalized = false, operation,
config, passesCondition,
data, path,
field, preferences,
filter, state,
forceFullValue = false, t,
fullData, user,
includeSchema = false, }: Args): Promise<void> => {
locale,
omitParents = false,
operation,
passesCondition,
path,
preferences,
skipConditionChecks = false,
skipValidation = false,
state,
t,
user,
} = args
if (fieldAffectsData(field)) { if (fieldAffectsData(field)) {
const fieldState: FormField = { const fieldState: FormField = {
condition: field.admin?.condition, condition: field.admin?.condition,
fieldSchema: includeSchema ? field : undefined,
initialValue: undefined, initialValue: undefined,
passesCondition, passesCondition,
valid: true, valid: true,
@@ -107,9 +66,9 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
data[field.name] = valueWithDefault data[field.name] = valueWithDefault
} }
let validationResult: string | true = true let validationResult: boolean | string = true
if (typeof fieldState.validate === 'function' && !skipValidation) { if (typeof fieldState.validate === 'function') {
validationResult = await fieldState.validate(data?.[field.name], { validationResult = await fieldState.validate(data?.[field.name], {
...field, ...field,
id, id,
@@ -137,36 +96,24 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
const rowPath = `${path}${field.name}.${i}.` const rowPath = `${path}${field.name}.${i}.`
row.id = row?.id || new ObjectID().toHexString() row.id = row?.id || new ObjectID().toHexString()
if (!omitParents && (!filter || filter(args))) { state[`${rowPath}id`] = {
state[`${rowPath}id`] = { initialValue: row.id,
fieldSchema: includeSchema valid: true,
? field.fields.find((field) => 'name' in field && field.name === 'id') value: row.id,
: undefined,
initialValue: row.id,
valid: true,
value: row.id,
}
} }
acc.promises.push( acc.promises.push(
iterateFields({ iterateFields({
id, id,
anyParentLocalized: field.localized || anyParentLocalized,
config, config,
data: row, data: row,
fields: field.fields, fields: field.fields,
filter,
forceFullValue,
fullData, fullData,
includeSchema,
locale, locale,
omitParents,
operation, operation,
parentPassesCondition: passesCondition, parentPassesCondition: passesCondition,
path: rowPath, path: rowPath,
preferences, preferences,
skipConditionChecks,
skipValidation,
state, state,
t, t,
user, user,
@@ -199,8 +146,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
fieldState.value = null fieldState.value = null
fieldState.initialValue = null fieldState.initialValue = null
} else { } else {
fieldState.value = forceFullValue ? arrayValue : arrayValue.length fieldState.value = arrayValue.length
fieldState.initialValue = forceFullValue ? arrayValue : arrayValue.length fieldState.initialValue = arrayValue.length
if (arrayValue.length > 0) { if (arrayValue.length > 0) {
fieldState.disableFormData = true fieldState.disableFormData = true
@@ -210,9 +157,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
fieldState.rows = rowMetadata fieldState.rows = rowMetadata
// Add field to state // Add field to state
if (!omitParents && (!filter || filter(args))) { state[`${path}${field.name}`] = fieldState
state[`${path}${field.name}`] = fieldState
}
break break
} }
@@ -228,60 +173,36 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
if (block) { if (block) {
row.id = row?.id || new ObjectID().toHexString() row.id = row?.id || new ObjectID().toHexString()
if (!omitParents && (!filter || filter(args))) { state[`${rowPath}id`] = {
state[`${rowPath}id`] = { initialValue: row.id,
fieldSchema: includeSchema valid: true,
? block.fields.find( value: row.id,
(blockField) => 'name' in blockField && blockField.name === 'id', }
)
: undefined,
initialValue: row.id,
valid: true,
value: row.id,
}
state[`${rowPath}blockType`] = { state[`${rowPath}blockType`] = {
fieldSchema: includeSchema initialValue: row.blockType,
? block.fields.find( valid: true,
(blockField) => 'name' in blockField && blockField.name === 'blockType', value: row.blockType,
) }
: undefined,
initialValue: row.blockType,
valid: true,
value: row.blockType,
}
state[`${rowPath}blockName`] = { state[`${rowPath}blockName`] = {
fieldSchema: includeSchema initialValue: row.blockName,
? block.fields.find( valid: true,
(blockField) => 'name' in blockField && blockField.name === 'blockName', value: row.blockName,
)
: undefined,
initialValue: row.blockName,
valid: true,
value: row.blockName,
}
} }
acc.promises.push( acc.promises.push(
iterateFields({ iterateFields({
id, id,
anyParentLocalized: field.localized || anyParentLocalized,
config, config,
data: row, data: row,
fields: block.fields, fields: block.fields,
filter,
forceFullValue,
fullData, fullData,
includeSchema,
locale, locale,
omitParents,
operation, operation,
parentPassesCondition: passesCondition, parentPassesCondition: passesCondition,
path: rowPath, path: rowPath,
preferences, preferences,
skipConditionChecks,
skipValidation,
state, state,
t, t,
user, user,
@@ -316,8 +237,8 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
fieldState.value = null fieldState.value = null
fieldState.initialValue = null fieldState.initialValue = null
} else { } else {
fieldState.value = forceFullValue ? blocksValue : blocksValue.length fieldState.value = blocksValue.length
fieldState.initialValue = forceFullValue ? blocksValue : blocksValue.length fieldState.initialValue = blocksValue.length
if (blocksValue.length > 0) { if (blocksValue.length > 0) {
fieldState.disableFormData = true fieldState.disableFormData = true
@@ -327,9 +248,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
fieldState.rows = rowMetadata fieldState.rows = rowMetadata
// Add field to state // Add field to state
if (!omitParents && (!filter || filter(args))) { state[`${path}${field.name}`] = fieldState
state[`${path}${field.name}`] = fieldState
}
break break
} }
@@ -337,22 +256,15 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
case 'group': { case 'group': {
await iterateFields({ await iterateFields({
id, id,
anyParentLocalized: field.localized || anyParentLocalized,
config, config,
data: data?.[field.name] || {}, data: data?.[field.name] || {},
fields: field.fields, fields: field.fields,
filter,
forceFullValue,
fullData, fullData,
includeSchema,
locale, locale,
omitParents,
operation, operation,
parentPassesCondition: passesCondition, parentPassesCondition: passesCondition,
path: `${path}${field.name}.`, path: `${path}${field.name}.`,
preferences, preferences,
skipConditionChecks,
skipValidation,
state, state,
t, t,
user, user,
@@ -412,9 +324,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
fieldState.initialValue = relationshipValue fieldState.initialValue = relationshipValue
} }
if (!filter || filter(args)) { state[`${path}${field.name}`] = fieldState
state[`${path}${field.name}`] = fieldState
}
break break
} }
@@ -427,9 +337,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
fieldState.value = relationshipValue fieldState.value = relationshipValue
fieldState.initialValue = relationshipValue fieldState.initialValue = relationshipValue
if (!filter || filter(args)) { state[`${path}${field.name}`] = fieldState
state[`${path}${field.name}`] = fieldState
}
break break
} }
@@ -439,9 +347,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
fieldState.initialValue = valueWithDefault fieldState.initialValue = valueWithDefault
// Add field to state // Add field to state
if (!filter || filter(args)) { state[`${path}${field.name}`] = fieldState
state[`${path}${field.name}`] = fieldState
}
break break
} }
@@ -450,22 +356,15 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
// Handle field types that do not use names (row, etc) // Handle field types that do not use names (row, etc)
await iterateFields({ await iterateFields({
id, id,
anyParentLocalized: field.localized || anyParentLocalized,
config, config,
data, data,
fields: field.fields, fields: field.fields,
filter,
forceFullValue,
fullData, fullData,
includeSchema,
locale, locale,
omitParents,
operation, operation,
parentPassesCondition: passesCondition, parentPassesCondition: passesCondition,
path, path,
preferences, preferences,
skipConditionChecks,
skipValidation,
state, state,
t, t,
user, user,
@@ -474,22 +373,15 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
const promises = field.tabs.map((tab) => const promises = field.tabs.map((tab) =>
iterateFields({ iterateFields({
id, id,
anyParentLocalized: tab.localized || anyParentLocalized,
config, config,
data: tabHasName(tab) ? data?.[tab.name] : data, data: tabHasName(tab) ? data?.[tab.name] : data,
fields: tab.fields, fields: tab.fields,
filter,
forceFullValue,
fullData, fullData,
includeSchema,
locale, locale,
omitParents,
operation, operation,
parentPassesCondition: passesCondition, parentPassesCondition: passesCondition,
path: tabHasName(tab) ? `${path}${tab.name}.` : path, path: tabHasName(tab) ? `${path}${tab.name}.` : path,
preferences, preferences,
skipConditionChecks,
skipValidation,
state, state,
t, t,
user, user,

View File

@@ -4,123 +4,65 @@ import type { User } from '../../../../../auth'
import type { SanitizedConfig } from '../../../../../config/types' import type { SanitizedConfig } from '../../../../../config/types'
import type { Field as FieldSchema } from '../../../../../fields/config/types' import type { Field as FieldSchema } from '../../../../../fields/config/types'
import type { Data, Fields } from '../types' import type { Data, Fields } from '../types'
import type { AddFieldStatePromiseArgs } from './addFieldStatePromise'
import { fieldIsPresentationalOnly } from '../../../../../fields/config/types' import { fieldIsPresentationalOnly } from '../../../../../fields/config/types'
import { addFieldStatePromise } from './addFieldStatePromise' import { addFieldStatePromise } from './addFieldStatePromise'
type Args = { 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 data: Data
fields: FieldSchema[] 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 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 locale: string
/**
* Whether to omit parent fields in the state. @default false
*/
omitParents?: boolean
/**
* operation is only needed for validation
*/
operation: 'create' | 'update' operation: 'create' | 'update'
parentPassesCondition?: boolean parentPassesCondition: boolean
/** path: string
* The initial path of the field. @default '' preferences: {
*/
path?: string
preferences?: {
[key: string]: unknown [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 t: TFunction
user: User user: User
} }
/**
* Flattens the fields schema and fields data
*/
export const iterateFields = async ({ export const iterateFields = async ({
id, id,
anyParentLocalized = false,
config, config,
data, data,
fields, fields,
filter,
forceFullValue = false,
fullData, fullData,
includeSchema = false,
locale, locale,
omitParents = false,
operation, operation,
parentPassesCondition = true, parentPassesCondition,
path = '', path = '',
preferences, preferences,
skipConditionChecks = false, state,
skipValidation = false,
state = {},
t, t,
user, user,
}: Args): Promise<void> => { }: Args): Promise<void> => {
const promises = [] const promises = []
fields.forEach((field) => { fields.forEach((field) => {
const initialData = data
if (!fieldIsPresentationalOnly(field) && !field?.admin?.disabled) { if (!fieldIsPresentationalOnly(field) && !field?.admin?.disabled) {
let passesCondition = true const passesCondition = Boolean(
if (!skipConditionChecks) { (field?.admin?.condition
passesCondition = Boolean( ? Boolean(field.admin.condition(fullData || {}, initialData || {}, { user }))
(field?.admin?.condition : true) && parentPassesCondition,
? Boolean(field.admin.condition(fullData || {}, data || {}, { user })) )
: true) && parentPassesCondition,
)
}
promises.push( promises.push(
addFieldStatePromise({ addFieldStatePromise({
id, id,
anyParentLocalized,
config, config,
data, data,
field, field,
filter,
forceFullValue,
fullData, fullData,
includeSchema,
locale, locale,
omitParents,
operation, operation,
passesCondition, passesCondition,
path, path,
preferences, preferences,
skipConditionChecks,
skipValidation,
state, state,
t, t,
user, user,

View File

@@ -2,22 +2,11 @@ import { unflatten as flatleyUnflatten } from 'flatley'
import type { Data, Fields } from './types' 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 = {} const data = {}
Object.keys(fields).forEach((key) => { Object.keys(fields).forEach((key) => {
if (ignoreDisableFormData === true || !fields[key].disableFormData) { if (!fields[key].disableFormData) {
data[key] = fields[key].value data[key] = fields[key].value
} }
}) })

View File

@@ -20,7 +20,6 @@ export type FormField = {
condition?: Condition condition?: Condition
disableFormData?: boolean disableFormData?: boolean
errorMessage?: string errorMessage?: string
fieldSchema?: FieldConfig
initialValue: unknown initialValue: unknown
passesCondition?: boolean passesCondition?: boolean
rows?: Row[] rows?: Row[]

View File

@@ -13,6 +13,7 @@
& > .field-type { & > .field-type {
margin-bottom: var(--spacing-field); margin-bottom: var(--spacing-field);
max-width: 100%;
&[type='hidden'] { &[type='hidden'] {
margin-bottom: 0; margin-bottom: 0;

View File

@@ -151,7 +151,7 @@ const NumberField: React.FC<Props> = (props) => {
if (isOverHasMany) { if (isOverHasMany) {
return t('validation:limitReached', { max: maxRows, value: value.length + 1 }) return t('validation:limitReached', { max: maxRows, value: value.length + 1 })
} }
return null return t('general:noOptions')
}} }}
numberOnly numberOnly
onChange={handleHasManyChange} onChange={handleHasManyChange}
@@ -170,7 +170,7 @@ const NumberField: React.FC<Props> = (props) => {
onChange={handleChange} onChange={handleChange}
onWheel={(e) => { onWheel={(e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error // @ts-ignore
e.target.blur() e.target.blur()
}} }}
placeholder={getTranslation(placeholder, i18n)} placeholder={getTranslation(placeholder, i18n)}

View File

@@ -9,7 +9,7 @@ const reduceToIDs = (options) =>
return [...ids, ...reduceToIDs(option.options)] return [...ids, ...reduceToIDs(option.options)]
} }
return [...ids, { id: option.value, relationTo: option.relationTo }] return [...ids, option.value]
}, []) }, [])
const sortOptions = (options: Option[]): Option[] => const sortOptions = (options: Option[]): Option[] =>
@@ -63,12 +63,10 @@ const optionsReducer = (state: OptionGroup[], action: Action): OptionGroup[] =>
const optionsToAddTo = newOptions.find( const optionsToAddTo = newOptions.find(
(optionGroup) => optionGroup.label === collection.labels.plural, (optionGroup) => optionGroup.label === collection.labels.plural,
) )
const newSubOptions = docs.reduce((docSubOptions, doc) => { const newSubOptions = docs.reduce((docSubOptions, doc) => {
if ( if (loadedIDs.indexOf(doc.id) === -1) {
loadedIDs.filter((item) => item.id === doc.id && item.relationTo === relation).length === loadedIDs.push(doc.id)
0
) {
loadedIDs.push({ id: doc.id, relationTo: relation })
const docTitle = formatUseAsTitle({ const docTitle = formatUseAsTitle({
collection, collection,
@@ -91,10 +89,7 @@ const optionsReducer = (state: OptionGroup[], action: Action): OptionGroup[] =>
}, []) }, [])
ids.forEach((id) => { ids.forEach((id) => {
if ( if (!loadedIDs.includes(id)) {
loadedIDs.filter((item) => item.id === id && item.relationTo === relation).length === 0
) {
loadedIDs.push({ id, relationTo: relation })
newSubOptions.push({ newSubOptions.push({
label: `${i18n.t('general:untitled')} - ID: ${id}`, label: `${i18n.t('general:untitled')} - ID: ${id}`,
relationTo: relation, relationTo: relation,

View File

@@ -29,14 +29,9 @@ type RichTextAdapterBase<
}) => Promise<void> | null }) => Promise<void> | null
outputSchema?: ({ outputSchema?: ({
field, field,
interfaceNameDefinitions,
isRequired, isRequired,
}: { }: {
field: RichTextField<Value, AdapterProps, ExtraFieldProperties> 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 isRequired: boolean
}) => JSONSchema4 }) => JSONSchema4
populationPromise?: (data: { populationPromise?: (data: {

View File

@@ -110,7 +110,7 @@ const TextInput: React.FC<TextInputProps> = (props) => {
if (isOverHasMany) { if (isOverHasMany) {
return t('validation:limitReached', { max: maxRows, value: value.length + 1 }) return t('validation:limitReached', { max: maxRows, value: value.length + 1 })
} }
return null return t('general:noOptions')
}} }}
onChange={onChange} onChange={onChange}
options={[]} options={[]}

View File

@@ -137,7 +137,6 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
fieldBaseClass, fieldBaseClass,
baseClass, baseClass,
className, className,
`field-${path.replace(/\./g, '__')}`,
showError && 'error', showError && 'error',
readOnly && 'read-only', readOnly && 'read-only',
] ]

View File

@@ -2,7 +2,6 @@
.upload { .upload {
position: relative; position: relative;
max-width: 100%;
&__wrap { &__wrap {
background: var(--theme-elevation-50); background: var(--theme-elevation-50);

View File

@@ -40,6 +40,7 @@ export const DocumentInfoProvider: React.FC<Props> = ({
const [publishedDoc, setPublishedDoc] = useState<TypeWithID & TypeWithTimestamps>(null) const [publishedDoc, setPublishedDoc] = useState<TypeWithID & TypeWithTimestamps>(null)
const [versions, setVersions] = useState<PaginatedDocs<Version>>(null) const [versions, setVersions] = useState<PaginatedDocs<Version>>(null)
const [unpublishedVersions, setUnpublishedVersions] = useState<PaginatedDocs<Version>>(null) const [unpublishedVersions, setUnpublishedVersions] = useState<PaginatedDocs<Version>>(null)
const [docPermissions, setDocPermissions] = useState<DocumentPermissions>(null)
const baseURL = `${serverURL}${api}` const baseURL = `${serverURL}${api}`
let slug: string let slug: string
@@ -61,10 +62,6 @@ export const DocumentInfoProvider: React.FC<Props> = ({
} }
} }
const [docPermissions, setDocPermissions] = useState<DocumentPermissions>(
permissions[pluralType][slug],
)
const getVersions = useCallback(async () => { const getVersions = useCallback(async () => {
let versionFetchURL let versionFetchURL
let publishedFetchURL let publishedFetchURL
@@ -218,14 +215,14 @@ export const DocumentInfoProvider: React.FC<Props> = ({
'Accept-Language': i18n.language, 'Accept-Language': i18n.language,
}, },
}) })
try { const json = await res.json()
const json = await res.json() setDocPermissions(json)
setDocPermissions(json) } else {
} catch (e) { // fallback to permissions from the entity type
console.error('Unable to fetch document permissions', e) // (i.e. create has no id)
} setDocPermissions(permissions[pluralType][slug])
} }
}, [serverURL, api, pluralType, slug, id, i18n.language, code]) }, [serverURL, api, pluralType, slug, id, permissions, i18n.language, code])
const getDocPreferences = useCallback(async () => { const getDocPreferences = useCallback(async () => {
return getPreference<DocumentPreferences>(preferencesKey) return getPreference<DocumentPreferences>(preferencesKey)
@@ -265,7 +262,6 @@ export const DocumentInfoProvider: React.FC<Props> = ({
const value: ContextType = { const value: ContextType = {
id, id,
slug,
collection, collection,
docPermissions, docPermissions,
getDocPermissions, getDocPermissions,
@@ -275,6 +271,7 @@ export const DocumentInfoProvider: React.FC<Props> = ({
preferencesKey, preferencesKey,
publishedDoc, publishedDoc,
setDocFieldPreferences, setDocFieldPreferences,
slug,
unpublishedVersions, unpublishedVersions,
versions, versions,
} }

View File

@@ -12,7 +12,7 @@ import type { TypeWithVersion } from '../../../../versions/types'
export type Version = TypeWithVersion<any> export type Version = TypeWithVersion<any>
export type DocumentPermissions = CollectionPermission | GlobalPermission export type DocumentPermissions = CollectionPermission | GlobalPermission | null
export type ContextType = { export type ContextType = {
collection?: SanitizedCollectionConfig collection?: SanitizedCollectionConfig

View File

@@ -17,9 +17,9 @@ export type globalViewType =
| 'Version' | 'Version'
| 'Versions' | 'Versions'
export const defaultGlobalViews = (): { export const defaultGlobalViews: {
[key in globalViewType]: React.ComponentType<any> [key in globalViewType]: React.ComponentType<any>
} => ({ } = {
API, API,
Default: DefaultGlobalEdit, Default: DefaultGlobalEdit,
LivePreview: LivePreviewView, LivePreview: LivePreviewView,
@@ -27,7 +27,7 @@ export const defaultGlobalViews = (): {
Relationships: null, Relationships: null,
Version: VersionView, Version: VersionView,
Versions: VersionsView, Versions: VersionsView,
}) }
export const CustomGlobalComponent = ( export const CustomGlobalComponent = (
args: GlobalEditViewProps & { args: GlobalEditViewProps & {
@@ -43,14 +43,18 @@ export const CustomGlobalComponent = (
// For example, the Edit view: // For example, the Edit view:
// 1. Edit?.Default // 1. Edit?.Default
// 2. Edit?.Default?.Component // 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 = const Component =
typeof Edit === 'object' && typeof Edit[view] === 'function' typeof Edit === 'object' && typeof Edit[view] === 'function'
? Edit[view] ? Edit[view]
: typeof Edit === 'object' && : typeof Edit === 'object' &&
typeof Edit?.[view] === 'object' && typeof Edit?.[view] === 'object' &&
// @ts-ignore
typeof Edit[view].Component === 'function' typeof Edit[view].Component === 'function'
? Edit[view].Component ? // @ts-ignore
: defaultGlobalViews()[view] Edit[view].Component
: defaultGlobalViews[view]
if (Component) { if (Component) {
return <Component {...args} /> return <Component {...args} />

View File

@@ -17,9 +17,9 @@ export type collectionViewType =
| 'Version' | 'Version'
| 'Versions' | 'Versions'
export const defaultCollectionViews = (): { export const defaultCollectionViews: {
[key in collectionViewType]: React.ComponentType<any> [key in collectionViewType]: React.ComponentType<any>
} => ({ } = {
API, API,
Default: DefaultCollectionEdit, Default: DefaultCollectionEdit,
LivePreview: LivePreviewView, LivePreview: LivePreviewView,
@@ -27,7 +27,7 @@ export const defaultCollectionViews = (): {
Relationships: null, Relationships: null,
Version: VersionView, Version: VersionView,
Versions: VersionsView, Versions: VersionsView,
}) }
export const CustomCollectionComponent = ( export const CustomCollectionComponent = (
args: CollectionEditViewProps & { args: CollectionEditViewProps & {
@@ -43,15 +43,18 @@ export const CustomCollectionComponent = (
// For example, the Edit view: // For example, the Edit view:
// 1. Edit?.Default // 1. Edit?.Default
// 2. Edit?.Default?.Component // 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 = const Component =
typeof Edit === 'object' && typeof Edit[view] === 'function' typeof Edit === 'object' && typeof Edit[view] === 'function'
? Edit[view] ? Edit[view]
: typeof Edit === 'object' && : typeof Edit === 'object' &&
typeof Edit?.[view] === 'object' && typeof Edit?.[view] === 'object' &&
// @ts-ignore
typeof Edit[view].Component === 'function' typeof Edit[view].Component === 'function'
? Edit[view].Component ? // @ts-ignore
: defaultCollectionViews()[view] Edit[view].Component
: defaultCollectionViews[view]
if (Component) { if (Component) {
return <Component {...args} /> return <Component {...args} />

View File

@@ -74,22 +74,21 @@ const DefaultCell: React.FC<Props> = (props) => {
if (collection.upload && fieldAffectsData(field) && field.name === 'filename') { if (collection.upload && fieldAffectsData(field) && field.name === 'filename') {
CellComponent = cellComponents.File CellComponent = cellComponents.File
} else { } else {
if (!cellData && 'label' in field) { return (
return ( <WrapElement {...wrapElementProps}>
<WrapElement {...wrapElementProps}> {(cellData === '' || typeof cellData === 'undefined') &&
{t('noLabel', { 'label' in field &&
t('noLabel', {
label: getTranslation( label: getTranslation(
typeof field.label === 'function' ? 'data' : field.label || 'data', typeof field.label === 'function' ? 'data' : field.label || 'data',
i18n, i18n,
), ),
})} })}
</WrapElement> {typeof cellData === 'string' && cellData}
) {typeof cellData === 'number' && cellData}
} else if (typeof cellData === 'string' || typeof cellData === 'number') { {typeof cellData === 'object' && JSON.stringify(cellData)}
return <WrapElement {...wrapElementProps}>{cellData}</WrapElement> </WrapElement>
} else if (typeof cellData === 'object') { )
return <WrapElement {...wrapElementProps}>{JSON.stringify(cellData)}</WrapElement>
}
} }
} }

View File

@@ -29,38 +29,37 @@ async function forgotPassword(incomingArgs: Arguments): Promise<null | string> {
let args = incomingArgs let args = incomingArgs
// /////////////////////////////////////
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection?.config,
context: args.req.context,
operation: 'forgotPassword',
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },
data,
disableEmail,
expiration,
req: {
payload: { config, emailOptions, sendEmail: email },
payload,
t,
},
req,
} = args
try { try {
const shouldCommit = await initTransaction(args.req) const shouldCommit = await initTransaction(req)
// /////////////////////////////////////
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection?.config,
context: args.req.context,
operation: 'forgotPassword',
req: args.req,
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },
data,
disableEmail,
expiration,
req: {
payload: { config, emailOptions, sendEmail: email },
payload,
t,
},
req,
} = args
// ///////////////////////////////////// // /////////////////////////////////////
// Forget password // Forget password
@@ -160,7 +159,7 @@ async function forgotPassword(incomingArgs: Arguments): Promise<null | string> {
return token return token
} catch (error: unknown) { } catch (error: unknown) {
await killTransaction(args.req) await killTransaction(req)
throw error throw error
} }
} }

View File

@@ -3,8 +3,10 @@ import type { PayloadRequest } from '../../../express/types'
import type { Payload } from '../../../payload' import type { Payload } from '../../../payload'
import type { Result } from '../forgotPassword' import type { Result } from '../forgotPassword'
import { getDataLoader } from '../../../collections/dataloader'
import { APIError } from '../../../errors' import { APIError } from '../../../errors'
import { createLocalReq } from '../../../utilities/createLocalReq' import { setRequestContext } from '../../../express/setRequestContext'
import { i18nInit } from '../../../translations/init'
import forgotPassword from '../forgotPassword' import forgotPassword from '../forgotPassword'
export type Options<T extends keyof GeneratedTypes['collections']> = { export type Options<T extends keyof GeneratedTypes['collections']> = {
@@ -22,7 +24,15 @@ async function localForgotPassword<T extends keyof GeneratedTypes['collections']
payload: Payload, payload: Payload,
options: Options<T>, options: Options<T>,
): Promise<Result> { ): Promise<Result> {
const { collection: collectionSlug, data, disableEmail, expiration } = options const {
collection: collectionSlug,
context,
data,
disableEmail,
expiration,
req = {} as PayloadRequest,
} = options
setRequestContext(req, context)
const collection = payload.collections[collectionSlug] const collection = payload.collections[collectionSlug]
@@ -34,7 +44,12 @@ async function localForgotPassword<T extends keyof GeneratedTypes['collections']
) )
} }
const req = createLocalReq(options, payload) req.payloadAPI = req.payloadAPI || 'local'
req.payload = payload
req.i18n = i18nInit(payload.config.i18n)
if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
return forgotPassword({ return forgotPassword({
collection, collection,

View File

@@ -5,8 +5,10 @@ import type { GeneratedTypes } from '../../../index'
import type { Payload } from '../../../payload' import type { Payload } from '../../../payload'
import type { Result } from '../login' import type { Result } from '../login'
import { getDataLoader } from '../../../collections/dataloader'
import { APIError } from '../../../errors' import { APIError } from '../../../errors'
import { createLocalReq } from '../../../utilities/createLocalReq' import { setRequestContext } from '../../../express/setRequestContext'
import { i18nInit } from '../../../translations/init'
import login from '../login' import login from '../login'
export type Options<TSlug extends keyof GeneratedTypes['collections']> = { export type Options<TSlug extends keyof GeneratedTypes['collections']> = {
@@ -31,14 +33,25 @@ async function localLogin<TSlug extends keyof GeneratedTypes['collections']>(
): Promise<Result & { user: GeneratedTypes['collections'][TSlug] }> { ): Promise<Result & { user: GeneratedTypes['collections'][TSlug] }> {
const { const {
collection: collectionSlug, collection: collectionSlug,
context,
data, data,
depth, depth,
fallbackLocale: fallbackLocaleArg = options?.req?.fallbackLocale,
locale: localeArg = null,
overrideAccess = true, overrideAccess = true,
req = {} as PayloadRequest,
res, res,
showHiddenFields, showHiddenFields,
} = options } = options
setRequestContext(req, context)
const collection = payload.collections[collectionSlug] const collection = payload.collections[collectionSlug]
const localizationConfig = payload?.config?.localization
const defaultLocale = localizationConfig ? localizationConfig.defaultLocale : null
const locale = localeArg || req?.locale || defaultLocale
const fallbackLocale = localizationConfig
? localizationConfig.locales.find(({ code }) => locale === code)?.fallbackLocale
: null
if (!collection) { if (!collection) {
throw new APIError( throw new APIError(
@@ -46,7 +59,12 @@ async function localLogin<TSlug extends keyof GeneratedTypes['collections']>(
) )
} }
const req = createLocalReq(options, payload) req.payloadAPI = req.payloadAPI || 'local'
req.payload = payload
req.i18n = i18nInit(payload.config.i18n)
if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
const args = { const args = {
collection, collection,
@@ -58,6 +76,12 @@ async function localLogin<TSlug extends keyof GeneratedTypes['collections']>(
showHiddenFields, showHiddenFields,
} }
if (locale) args.req.locale = locale
if (fallbackLocale) {
args.req.fallbackLocale =
typeof fallbackLocaleArg !== 'undefined' ? fallbackLocaleArg : fallbackLocale || defaultLocale
}
return login<TSlug>(args) return login<TSlug>(args)
} }

View File

@@ -3,8 +3,10 @@ import type { PayloadRequest } from '../../../express/types'
import type { Payload } from '../../../payload' import type { Payload } from '../../../payload'
import type { Result } from '../resetPassword' import type { Result } from '../resetPassword'
import { getDataLoader } from '../../../collections/dataloader'
import { APIError } from '../../../errors' import { APIError } from '../../../errors'
import { createLocalReq } from '../../../utilities/createLocalReq' import { setRequestContext } from '../../../express/setRequestContext'
import { i18nInit } from '../../../translations/init'
import resetPassword from '../resetPassword' import resetPassword from '../resetPassword'
export type Options<T extends keyof GeneratedTypes['collections']> = { export type Options<T extends keyof GeneratedTypes['collections']> = {
@@ -22,7 +24,15 @@ async function localResetPassword<T extends keyof GeneratedTypes['collections']>
payload: Payload, payload: Payload,
options: Options<T>, options: Options<T>,
): Promise<Result> { ): Promise<Result> {
const { collection: collectionSlug, data, overrideAccess } = options const {
collection: collectionSlug,
context,
data,
overrideAccess,
req = {} as PayloadRequest,
} = options
setRequestContext(req, context)
const collection = payload.collections[collectionSlug] const collection = payload.collections[collectionSlug]
@@ -34,7 +44,12 @@ async function localResetPassword<T extends keyof GeneratedTypes['collections']>
) )
} }
const req = createLocalReq(options, payload) req.payload = payload
req.payloadAPI = req.payloadAPI || 'local'
req.i18n = i18nInit(payload.config.i18n)
if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
return resetPassword({ return resetPassword({
collection, collection,

View File

@@ -2,8 +2,10 @@ import type { GeneratedTypes, RequestContext } from '../../../'
import type { PayloadRequest } from '../../../express/types' import type { PayloadRequest } from '../../../express/types'
import type { Payload } from '../../../payload' import type { Payload } from '../../../payload'
import { getDataLoader } from '../../../collections/dataloader'
import { APIError } from '../../../errors' import { APIError } from '../../../errors'
import { createLocalReq } from '../../../utilities/createLocalReq' import { setRequestContext } from '../../../express/setRequestContext'
import { i18nInit } from '../../../translations/init'
import unlock from '../unlock' import unlock from '../unlock'
export type Options<T extends keyof GeneratedTypes['collections']> = { export type Options<T extends keyof GeneratedTypes['collections']> = {
@@ -20,7 +22,14 @@ async function localUnlock<T extends keyof GeneratedTypes['collections']>(
payload: Payload, payload: Payload,
options: Options<T>, options: Options<T>,
): Promise<boolean> { ): Promise<boolean> {
const { collection: collectionSlug, data, overrideAccess = true } = options const {
collection: collectionSlug,
context,
data,
overrideAccess = true,
req = {} as PayloadRequest,
} = options
setRequestContext(req, context)
const collection = payload.collections[collectionSlug] const collection = payload.collections[collectionSlug]
@@ -30,7 +39,12 @@ async function localUnlock<T extends keyof GeneratedTypes['collections']>(
) )
} }
const req = createLocalReq(options, payload) req.payload = payload
req.payloadAPI = req.payloadAPI || 'local'
req.i18n = i18nInit(payload.config.i18n)
if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
return unlock({ return unlock({
collection, collection,

View File

@@ -3,7 +3,8 @@ import type { PayloadRequest } from '../../../express/types'
import type { Payload } from '../../../payload' import type { Payload } from '../../../payload'
import { APIError } from '../../../errors' import { APIError } from '../../../errors'
import { createLocalReq } from '../../../utilities/createLocalReq' import { setRequestContext } from '../../../express/setRequestContext'
import { i18nInit } from '../../../translations/init'
import verifyEmail from '../verifyEmail' import verifyEmail from '../verifyEmail'
export type Options<T extends keyof GeneratedTypes['collections']> = { export type Options<T extends keyof GeneratedTypes['collections']> = {
@@ -17,7 +18,8 @@ async function localVerifyEmail<T extends keyof GeneratedTypes['collections']>(
payload: Payload, payload: Payload,
options: Options<T>, options: Options<T>,
): Promise<boolean> { ): Promise<boolean> {
const { collection: collectionSlug, token } = options const { collection: collectionSlug, context, req = {} as PayloadRequest, token } = options
setRequestContext(req, context)
const collection = payload.collections[collectionSlug] const collection = payload.collections[collectionSlug]
@@ -27,7 +29,9 @@ async function localVerifyEmail<T extends keyof GeneratedTypes['collections']>(
) )
} }
const req = createLocalReq(options, payload) req.payload = payload
req.payloadAPI = req.payloadAPI || 'local'
req.i18n = i18nInit(payload.config.i18n)
return verifyEmail({ return verifyEmail({
collection, collection,

View File

@@ -18,8 +18,8 @@ import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'
import isLocked from '../isLocked' import isLocked from '../isLocked'
import { authenticateLocalStrategy } from '../strategies/local/authenticate' import { authenticateLocalStrategy } from '../strategies/local/authenticate'
import { incrementLoginAttempts } from '../strategies/local/incrementLoginAttempts' import { incrementLoginAttempts } from '../strategies/local/incrementLoginAttempts'
import { resetLoginAttempts } from '../strategies/local/resetLoginAttempts'
import { getFieldsToSign } from './getFieldsToSign' import { getFieldsToSign } from './getFieldsToSign'
import unlock from './unlock'
export type Result = { export type Result = {
exp?: number exp?: number
@@ -45,38 +45,37 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
): Promise<Result & { user: GeneratedTypes['collections'][TSlug] }> { ): Promise<Result & { user: GeneratedTypes['collections'][TSlug] }> {
let args = incomingArgs let args = incomingArgs
// /////////////////////////////////////
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection?.config,
context: args.req.context,
operation: 'login',
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },
data,
depth,
overrideAccess,
req,
req: {
payload,
payload: { config, secret },
},
showHiddenFields,
} = args
try { try {
const shouldCommit = await initTransaction(args.req) const shouldCommit = await initTransaction(req)
// /////////////////////////////////////
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection?.config,
context: args.req.context,
operation: 'login',
req: args.req,
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },
data,
depth,
overrideAccess,
req,
req: {
payload,
payload: { config, secret },
},
showHiddenFields,
} = args
// ///////////////////////////////////// // /////////////////////////////////////
// Login // Login
@@ -116,16 +115,16 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
}) })
} }
if (shouldCommit) await commitTransaction(req)
throw new AuthenticationError(req.t) throw new AuthenticationError(req.t)
} }
if (maxLoginAttemptsEnabled) { if (maxLoginAttemptsEnabled) {
await resetLoginAttempts({ await unlock({
collection: collectionConfig, collection: {
doc: user, config: collectionConfig,
payload: req.payload, },
data,
overrideAccess: true,
req, req,
}) })
} }
@@ -263,7 +262,7 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
return result return result
} catch (error: unknown) { } catch (error: unknown) {
await killTransaction(args.req) await killTransaction(req)
throw error throw error
} }
} }

View File

@@ -9,10 +9,7 @@ import type { Document } from '../../types'
import { buildAfterOperation } from '../../collections/operations/utils' import { buildAfterOperation } from '../../collections/operations/utils'
import { Forbidden } from '../../errors' import { Forbidden } from '../../errors'
import { commitTransaction } from '../../utilities/commitTransaction'
import getCookieExpiration from '../../utilities/getCookieExpiration' import getCookieExpiration from '../../utilities/getCookieExpiration'
import { initTransaction } from '../../utilities/initTransaction'
import { killTransaction } from '../../utilities/killTransaction'
import { getFieldsToSign } from './getFieldsToSign' import { getFieldsToSign } from './getFieldsToSign'
export type Result = { export type Result = {
@@ -31,130 +28,120 @@ export type Arguments = {
async function refresh(incomingArgs: Arguments): Promise<Result> { async function refresh(incomingArgs: Arguments): Promise<Result> {
let args = incomingArgs let args = incomingArgs
try { // /////////////////////////////////////
const shouldCommit = await initTransaction(args.req) // beforeOperation - Collection
// /////////////////////////////////////
// ///////////////////////////////////// await args.collection.config.hooks.beforeOperation.reduce(
// beforeOperation - Collection async (priorHook: BeforeOperationHook | Promise<void>, hook: BeforeOperationHook) => {
// /////////////////////////////////////
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: 'refresh',
req: args.req,
})) || args
},
Promise.resolve(),
)
// /////////////////////////////////////
// Refresh
// /////////////////////////////////////
const {
collection: { config: collectionConfig },
req: {
payload: { config, secret },
},
} = args
if (typeof args.token !== 'string' || !args.req.user) throw new Forbidden(args.req.t)
const parsedURL = url.parse(args.req.url)
const isGraphQL = parsedURL.pathname === config.routes.graphQL
const user = await args.req.payload.findByID({
id: args.req.user.id,
collection: args.req.user.collection,
depth: isGraphQL ? 0 : args.collection.config.auth.depth,
req: args.req,
})
const fieldsToSign = getFieldsToSign({
collectionConfig,
email: user?.email as string,
user: args?.req?.user,
})
const refreshedToken = jwt.sign(fieldsToSign, secret, {
expiresIn: collectionConfig.auth.tokenExpiration,
})
const exp = (jwt.decode(refreshedToken) as Record<string, unknown>).exp as number
if (args.res) {
const cookieOptions = {
domain: undefined,
expires: getCookieExpiration(collectionConfig.auth.tokenExpiration),
httpOnly: true,
path: '/',
sameSite: collectionConfig.auth.cookies.sameSite,
secure: collectionConfig.auth.cookies.secure,
}
if (collectionConfig.auth.cookies.domain)
cookieOptions.domain = collectionConfig.auth.cookies.domain
args.res.cookie(`${config.cookiePrefix}-token`, refreshedToken, cookieOptions)
}
let result: Result = {
exp,
refreshedToken,
user,
}
// /////////////////////////////////////
// After Refresh - Collection
// /////////////////////////////////////
await collectionConfig.hooks.afterRefresh.reduce(async (priorHook, hook) => {
await priorHook await priorHook
result = args =
(await hook({ (await hook({
args,
collection: args.collection?.config, collection: args.collection?.config,
context: args.req.context, context: args.req.context,
exp, operation: 'refresh',
req: args.req, })) || args
res: args.res, },
token: refreshedToken, Promise.resolve(),
})) || result )
}, Promise.resolve())
// ///////////////////////////////////// // /////////////////////////////////////
// afterOperation - Collection // Refresh
// ///////////////////////////////////// // /////////////////////////////////////
result = await buildAfterOperation({ const {
args, collection: { config: collectionConfig },
collection: args.collection?.config, req: {
operation: 'refresh', payload: { config, secret },
result, },
}) } = args
// ///////////////////////////////////// if (typeof args.token !== 'string' || !args.req.user) throw new Forbidden(args.req.t)
// Return results
// /////////////////////////////////////
if (collectionConfig.auth.removeTokenFromResponses) { const parsedURL = url.parse(args.req.url)
delete result.refreshedToken const isGraphQL = parsedURL.pathname === config.routes.graphQL
const user = await args.req.payload.findByID({
id: args.req.user.id,
collection: args.req.user.collection,
depth: isGraphQL ? 0 : args.collection.config.auth.depth,
req: args.req,
})
const fieldsToSign = getFieldsToSign({
collectionConfig,
email: user?.email as string,
user: args?.req?.user,
})
const refreshedToken = jwt.sign(fieldsToSign, secret, {
expiresIn: collectionConfig.auth.tokenExpiration,
})
const exp = (jwt.decode(refreshedToken) as Record<string, unknown>).exp as number
if (args.res) {
const cookieOptions = {
domain: undefined,
expires: getCookieExpiration(collectionConfig.auth.tokenExpiration),
httpOnly: true,
path: '/',
sameSite: collectionConfig.auth.cookies.sameSite,
secure: collectionConfig.auth.cookies.secure,
} }
if (shouldCommit) await commitTransaction(args.req) if (collectionConfig.auth.cookies.domain)
cookieOptions.domain = collectionConfig.auth.cookies.domain
return result args.res.cookie(`${config.cookiePrefix}-token`, refreshedToken, cookieOptions)
} catch (error: unknown) {
await killTransaction(args.req)
throw error
} }
let result: Result = {
exp,
refreshedToken,
user,
}
// /////////////////////////////////////
// After Refresh - Collection
// /////////////////////////////////////
await collectionConfig.hooks.afterRefresh.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: args.collection?.config,
context: args.req.context,
exp,
req: args.req,
res: args.res,
token: refreshedToken,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = await buildAfterOperation({
args,
collection: args.collection?.config,
operation: 'refresh',
result,
})
// /////////////////////////////////////
// Return results
// /////////////////////////////////////
if (collectionConfig.auth.removeTokenFromResponses) {
delete result.refreshedToken
}
return result
} }
export default refresh export default refresh

View File

@@ -52,6 +52,5 @@ export const incrementLoginAttempts = async ({
id: doc.id, id: doc.id,
collection: collection.slug, collection: collection.slug,
data, data,
req,
}) })
} }

View File

@@ -15,7 +15,6 @@ export const resetLoginAttempts = async ({
payload, payload,
req, req,
}: Args): Promise<void> => { }: Args): Promise<void> => {
if (!('lockUntil' in doc && typeof doc.lockUntil === 'string') || doc.loginAttempts === 0) return
await payload.update({ await payload.update({
id: doc.id, id: doc.id,
collection: collection.slug, collection: collection.slug,
@@ -23,7 +22,6 @@ export const resetLoginAttempts = async ({
lockUntil: null, lockUntil: null,
loginAttempts: 0, loginAttempts: 0,
}, },
overrideAccess: true,
req, req,
}) })
} }

View File

@@ -31,10 +31,6 @@ export async function generateTypes(): Promise<void> {
style: { style: {
singleQuote: true, 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) => { }).then((compiled) => {
if (config.typescript.declare !== false) { if (config.typescript.declare !== false) {
compiled += `\n\n${declare}` compiled += `\n\n${declare}`

View File

@@ -45,7 +45,7 @@ if (tsConfig?.config?.compilerOptions?.paths) {
// Allow disabling SWC for debugging // Allow disabling SWC for debugging
if (process.env.DISABLE_SWC !== 'true') { if (process.env.DISABLE_SWC !== 'true') {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error - bad @swc/register types // @ts-ignore - bad @swc/register types
swcRegister(swcOptions) swcRegister(swcOptions)
} }

View File

@@ -27,7 +27,7 @@ const availableCommands = [
const availableCommandsMsg = `Available commands: ${availableCommands.join(', ')}` const availableCommandsMsg = `Available commands: ${availableCommands.join(', ')}`
export const migrate = async (parsedArgs: ParsedArgs): Promise<void> => { export const migrate = async (parsedArgs: ParsedArgs): Promise<void> => {
const { _: args, file, forceAcceptWarning, help } = parsedArgs const { _: args, file, help } = parsedArgs
if (help) { if (help) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`\n\n${availableCommandsMsg}\n`) // Avoid having to init payload to get the logger console.log(`\n\n${availableCommandsMsg}\n`) // Avoid having to init payload to get the logger
@@ -74,16 +74,11 @@ export const migrate = async (parsedArgs: ParsedArgs): Promise<void> => {
await adapter.migrateReset() await adapter.migrateReset()
break break
case 'migrate:fresh': case 'migrate:fresh':
await adapter.migrateFresh({ forceAcceptWarning }) await adapter.migrateFresh()
break break
case 'migrate:create': case 'migrate:create':
try { try {
await adapter.createMigration({ await adapter.createMigration({ file, migrationName: args[1], payload })
file,
forceAcceptWarning,
migrationName: args[1],
payload,
})
} catch (err) { } catch (err) {
throw new Error(`Error creating migration: ${err.message}`) throw new Error(`Error creating migration: ${err.message}`)
} }

View File

@@ -48,7 +48,6 @@ export type BeforeOperationHook = (args: {
* Hook operation being performed * Hook operation being performed
*/ */
operation: HookOperationType operation: HookOperationType
req: PayloadRequest
}) => any }) => any
export type BeforeValidateHook<T extends TypeWithID = any> = (args: { export type BeforeValidateHook<T extends TypeWithID = any> = (args: {

View File

@@ -55,45 +55,44 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
): Promise<GeneratedTypes['collections'][TSlug]> { ): Promise<GeneratedTypes['collections'][TSlug]> {
let args = incomingArgs let args = incomingArgs
// /////////////////////////////////////
// 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: 'create',
})) || args
},
Promise.resolve(),
)
const {
autosave = false,
collection: { config: collectionConfig },
collection,
depth,
disableVerificationEmail,
draft = false,
overrideAccess,
overwriteExistingFiles = false,
req: {
payload,
payload: { config, emailOptions },
},
req,
showHiddenFields,
} = args
try { try {
const shouldCommit = await initTransaction(args.req) const shouldCommit = await initTransaction(req)
// /////////////////////////////////////
// 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: 'create',
req: args.req,
})) || args
},
Promise.resolve(),
)
const {
autosave = false,
collection: { config: collectionConfig },
collection,
depth,
disableVerificationEmail,
draft = false,
overrideAccess,
overwriteExistingFiles = false,
req: {
payload,
payload: { config, emailOptions },
},
req,
showHiddenFields,
} = args
let { data } = args let { data } = args
@@ -368,7 +367,7 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
return result return result
} catch (error: unknown) { } catch (error: unknown) {
await killTransaction(args.req) await killTransaction(req)
throw error throw error
} }
} }

View File

@@ -39,42 +39,42 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
}> { }> {
let args = incomingArgs let args = incomingArgs
// /////////////////////////////////////
// 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: 'delete',
})) || args
},
Promise.resolve(),
)
const {
collection: { config: collectionConfig },
depth,
overrideAccess,
req: {
locale,
payload: { config },
payload,
t,
},
req,
showHiddenFields,
where,
} = args
try { try {
const shouldCommit = await initTransaction(args.req) const shouldCommit = await initTransaction(req)
// /////////////////////////////////////
// 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: 'delete',
req: args.req,
})) || args
},
Promise.resolve(),
)
const {
collection: { config: collectionConfig },
depth,
overrideAccess,
req: {
locale,
payload: { config },
payload,
t,
},
req,
showHiddenFields,
where,
} = args
if (!where) { if (!where) {
throw new APIError("Missing 'where' query of documents to delete.", httpStatus.BAD_REQUEST) throw new APIError("Missing 'where' query of documents to delete.", httpStatus.BAD_REQUEST)
@@ -264,7 +264,7 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
return result return result
} catch (error: unknown) { } catch (error: unknown) {
await killTransaction(args.req) await killTransaction(req)
throw error throw error
} }
} }

View File

@@ -30,42 +30,41 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(
): Promise<Document> { ): Promise<Document> {
let args = incomingArgs let args = incomingArgs
// /////////////////////////////////////
// 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: 'delete',
})) || args
},
Promise.resolve(),
)
const {
id,
collection: { config: collectionConfig },
depth,
overrideAccess,
req: {
payload: { config },
payload,
t,
},
req,
showHiddenFields,
} = args
try { try {
const shouldCommit = await initTransaction(args.req) const shouldCommit = await initTransaction(req)
// /////////////////////////////////////
// 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: 'delete',
req: args.req,
})) || args
},
Promise.resolve(),
)
const {
id,
collection: { config: collectionConfig },
depth,
overrideAccess,
req: {
payload: { config },
payload,
t,
},
req,
showHiddenFields,
} = args
// ///////////////////////////////////// // /////////////////////////////////////
// Access // Access
@@ -214,7 +213,7 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(
return result return result
} catch (error: unknown) { } catch (error: unknown) {
await killTransaction(args.req) await killTransaction(req)
throw error throw error
} }
} }

View File

@@ -37,43 +37,42 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
): Promise<PaginatedDocs<T>> { ): Promise<PaginatedDocs<T>> {
let args = incomingArgs let args = incomingArgs
// /////////////////////////////////////
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'read',
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },
collection,
currentDepth,
depth,
disableErrors,
draft: draftsEnabled,
limit,
overrideAccess,
page,
pagination = true,
req: { locale, payload },
req,
showHiddenFields,
sort,
where,
} = args
try { try {
const shouldCommit = await initTransaction(args.req) const shouldCommit = await initTransaction(req)
// /////////////////////////////////////
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'read',
req: args.req,
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },
collection,
currentDepth,
depth,
disableErrors,
draft: draftsEnabled,
limit,
overrideAccess,
page,
pagination = true,
req: { locale, payload },
req,
showHiddenFields,
sort,
where,
} = args
// ///////////////////////////////////// // /////////////////////////////////////
// Access // Access
@@ -254,7 +253,7 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
return result return result
} catch (error: unknown) { } catch (error: unknown) {
await killTransaction(args.req) await killTransaction(req)
throw error throw error
} }
} }

View File

@@ -30,39 +30,38 @@ export type Arguments = {
async function findByID<T extends TypeWithID>(incomingArgs: Arguments): Promise<T> { async function findByID<T extends TypeWithID>(incomingArgs: Arguments): Promise<T> {
let args = incomingArgs let args = incomingArgs
// /////////////////////////////////////
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'read',
})) || args
}, Promise.resolve())
const {
id,
collection: { config: collectionConfig },
currentDepth,
depth,
disableErrors,
draft: draftEnabled = false,
overrideAccess = false,
req: { locale, t },
req,
showHiddenFields,
} = args
try { try {
const shouldCommit = await initTransaction(args.req) const shouldCommit = await initTransaction(req)
const { transactionID } = args.req const { transactionID } = req
// /////////////////////////////////////
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'read',
req: args.req,
})) || args
}, Promise.resolve())
const {
id,
collection: { config: collectionConfig },
currentDepth,
depth,
disableErrors,
draft: draftEnabled = false,
overrideAccess = false,
req: { locale, t },
req,
showHiddenFields,
} = args
// ///////////////////////////////////// // /////////////////////////////////////
// Access // Access
@@ -205,7 +204,7 @@ async function findByID<T extends TypeWithID>(incomingArgs: Arguments): Promise<
return result return result
} catch (error: unknown) { } catch (error: unknown) {
await killTransaction(args.req) await killTransaction(req)
throw error throw error
} }
} }

View File

@@ -8,8 +8,10 @@ import type { Document } from '../../../types'
import type { File } from '../../../uploads/types' import type { File } from '../../../uploads/types'
import { APIError } from '../../../errors' import { APIError } from '../../../errors'
import { setRequestContext } from '../../../express/setRequestContext'
import { i18nInit } from '../../../translations/init'
import getFileByPath from '../../../uploads/getFileByPath' import getFileByPath from '../../../uploads/getFileByPath'
import { createLocalReq } from '../../../utilities/createLocalReq' import { getDataLoader } from '../../dataloader'
import create from '../create' import create from '../create'
export type Options<TSlug extends keyof GeneratedTypes['collections']> = { export type Options<TSlug extends keyof GeneratedTypes['collections']> = {
@@ -42,17 +44,30 @@ export default async function createLocal<TSlug extends keyof GeneratedTypes['co
): Promise<GeneratedTypes['collections'][TSlug]> { ): Promise<GeneratedTypes['collections'][TSlug]> {
const { const {
collection: collectionSlug, collection: collectionSlug,
context,
data, data,
depth, depth,
disableVerificationEmail, disableVerificationEmail,
draft, draft,
fallbackLocale: fallbackLocaleArg = options?.req?.fallbackLocale,
file, file,
filePath, filePath,
locale: localeArg = null,
overrideAccess = true, overrideAccess = true,
overwriteExistingFiles = false, overwriteExistingFiles = false,
req = {} as PayloadRequest,
showHiddenFields, showHiddenFields,
user,
} = options } = options
setRequestContext(req, context)
const collection = payload.collections[collectionSlug] const collection = payload.collections[collectionSlug]
const localizationConfig = payload?.config?.localization
const defaultLocale = localizationConfig ? localizationConfig.defaultLocale : null
const locale = localeArg || req.locale || defaultLocale
const fallbackLocale = localizationConfig
? localizationConfig.locales.find(({ code }) => locale === code)?.fallbackLocale
: null
if (!collection) { if (!collection) {
throw new APIError( throw new APIError(
@@ -60,18 +75,21 @@ export default async function createLocal<TSlug extends keyof GeneratedTypes['co
) )
} }
const req = createLocalReq(options, payload) req.payloadAPI = req.payloadAPI || 'local'
const fileToSet = (file ?? (await getFileByPath(filePath))) as UploadedFile req.locale = locale
if (fileToSet) { req.fallbackLocale =
if (req?.files) { typeof fallbackLocaleArg !== 'undefined' ? fallbackLocaleArg : fallbackLocale || defaultLocale
req.files.file = fileToSet req.payload = payload
} else { req.i18n = i18nInit(payload.config.i18n)
req.files = { req.files = {
file: fileToSet, file: (file ?? (await getFileByPath(filePath))) as UploadedFile,
}
}
} }
if (typeof user !== 'undefined') req.user = user
if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
return create<TSlug>({ return create<TSlug>({
collection, collection,
data, data,

View File

@@ -5,7 +5,9 @@ import type { Document, Where } from '../../../types'
import type { BulkOperationResult } from '../../config/types' import type { BulkOperationResult } from '../../config/types'
import { APIError } from '../../../errors' import { APIError } from '../../../errors'
import { createLocalReq } from '../../../utilities/createLocalReq' import { setRequestContext } from '../../../express/setRequestContext'
import { i18nInit } from '../../../translations/init'
import { getDataLoader } from '../../dataloader'
import deleteOperation from '../delete' import deleteOperation from '../delete'
import deleteByID from '../deleteByID' import deleteByID from '../deleteByID'
@@ -57,13 +59,24 @@ async function deleteLocal<TSlug extends keyof GeneratedTypes['collections']>(
const { const {
id, id,
collection: collectionSlug, collection: collectionSlug,
context,
depth, depth,
fallbackLocale: fallbackLocaleArg = options?.req?.fallbackLocale,
locale: localeArg = null,
overrideAccess = true, overrideAccess = true,
req: incomingReq = {} as PayloadRequest,
showHiddenFields, showHiddenFields,
user,
where, where,
} = options } = options
const collection = payload.collections[collectionSlug] const collection = payload.collections[collectionSlug]
const localizationConfig = payload?.config?.localization
const defaultLocale = localizationConfig ? localizationConfig.defaultLocale : null
const locale = localeArg || incomingReq?.locale || defaultLocale
const fallbackLocale = localizationConfig
? localizationConfig.locales.find(({ code }) => locale === code)?.fallbackLocale
: null
if (!collection) { if (!collection) {
throw new APIError( throw new APIError(
@@ -71,7 +84,22 @@ async function deleteLocal<TSlug extends keyof GeneratedTypes['collections']>(
) )
} }
const req = createLocalReq(options, payload) const req = {
fallbackLocale:
typeof fallbackLocaleArg !== 'undefined'
? fallbackLocaleArg
: fallbackLocale || defaultLocale,
i18n: i18nInit(payload.config.i18n),
locale: locale,
payload,
payloadAPI: 'local',
transactionID: incomingReq?.transactionID,
user,
} as PayloadRequest
setRequestContext(req, context)
if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
const args = { const args = {
id, id,

Some files were not shown because too many files have changed in this diff Show More