diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b4f0c7e20..53f970d88 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -3,7 +3,7 @@ module.exports = { overrides: [ { extends: ['plugin:@typescript-eslint/disable-type-checked'], - files: ['*.js', '*.cjs'], + files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'], }, { files: ['packages/eslint-config-payload/**'], diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml index 4aa924616..d08e0a87d 100644 --- a/.github/ISSUE_TEMPLATE/1.bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1.bug_report.yml @@ -1,6 +1,6 @@ name: Bug Report description: Create a bug report for Payload -labels: ["possible-bug"] +labels: ['possible-bug'] body: - type: markdown attributes: diff --git a/.github/reproduction-guide.md b/.github/reproduction-guide.md index 797f0b2cb..85ea565d7 100644 --- a/.github/reproduction-guide.md +++ b/.github/reproduction-guide.md @@ -9,6 +9,7 @@ **NOTE:** The goal is to isolate the problem by reducing the number of `collections/globals/fields` you add to the `test/_community` folder. This folder is _not_ meant for you to copy your project into, but rather recreate the issue you are experiencing with minimal config. ## Example test directory file tree + ```text . ├── config.ts @@ -27,9 +28,11 @@ The directory split up in this way specifically to reduce friction when creating
## Testing is optional but encouraged + An issue does not need to have failing tests — reproduction steps with your forked repo are enough at this point. Some people like to dive deeper and we want to give you the guidance/tools to do so. Read more below: ### Running integration tests (Payload API tests) + First install [Jest Runner for VSVode](https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner). There are a couple ways run integration tests: @@ -45,7 +48,9 @@ There are a couple ways run integration tests: ``` ### Running E2E tests (Admin Panel UI tests) + The easiest way to run E2E tests is to install + - [Playwright Test for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) - [Playwright Runner](https://marketplace.visualstudio.com/items?itemName=ortoni.ortoni) @@ -53,6 +58,6 @@ Once they are installed you can open the `testing` tab in vscode sidebar and dri - #### Notes + - It is recommended to add the test credentials (located in `test/credentials.ts`) to your autofill for `localhost:3000/admin` as this will be required on every nodemon restart. The default credentials are `dev@payloadcms.com` as email and `test` as password. diff --git a/.migrations/drizzle-snapshot.json b/.migrations/drizzle-snapshot.json index 7011c2298..801558cbd 100644 --- a/.migrations/drizzle-snapshot.json +++ b/.migrations/drizzle-snapshot.json @@ -1,922 +1,10 @@ { - "id": "4b6f2243-b055-45d8-9afa-53cd2b729a12", - "prevId": "00000000-0000-0000-0000-000000000000", - "version": "5", - "dialect": "pg", - "tables": { - "posts_my_array_my_sub_array": { - "name": "posts_my_array_my_sub_array", - "schema": "", - "columns": { - "_order": { - "name": "_order", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "_parent_id": { - "name": "_parent_id", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "id": { - "name": "id", - "type": "varchar", - "primaryKey": true, - "notNull": true - }, - "sub_sub_field": { - "name": "sub_sub_field", - "type": "varchar", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "posts_my_array_my_sub_array__parent_id_posts_my_array_id_fk": { - "name": "posts_my_array_my_sub_array__parent_id_posts_my_array_id_fk", - "tableFrom": "posts_my_array_my_sub_array", - "tableTo": "posts_my_array", - "columnsFrom": [ - "_parent_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "posts_my_array": { - "name": "posts_my_array", - "schema": "", - "columns": { - "_order": { - "name": "_order", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "_parent_id": { - "name": "_parent_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "id": { - "name": "id", - "type": "varchar", - "primaryKey": true, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "posts_my_array__parent_id_posts_id_fk": { - "name": "posts_my_array__parent_id_posts_id_fk", - "tableFrom": "posts_my_array", - "tableTo": "posts", - "columnsFrom": [ - "_parent_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "posts_my_array_locales": { - "name": "posts_my_array_locales", - "schema": "", - "columns": { - "sub_field": { - "name": "sub_field", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "_locale": { - "name": "_locale", - "type": "_locales", - "primaryKey": false, - "notNull": true - }, - "_parent_id": { - "name": "_parent_id", - "type": "varchar", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "posts_my_array_locales__parent_id_posts_my_array_id_fk": { - "name": "posts_my_array_locales__parent_id_posts_my_array_id_fk", - "tableFrom": "posts_my_array_locales", - "tableTo": "posts_my_array", - "columnsFrom": [ - "_parent_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "posts_block1": { - "name": "posts_block1", - "schema": "", - "columns": { - "_order": { - "name": "_order", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "_path": { - "name": "_path", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "_parent_id": { - "name": "_parent_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "id": { - "name": "id", - "type": "varchar", - "primaryKey": true, - "notNull": true - }, - "non_localized_text": { - "name": "non_localized_text", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "block_name": { - "name": "block_name", - "type": "varchar", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "posts_block1__parent_id_posts_id_fk": { - "name": "posts_block1__parent_id_posts_id_fk", - "tableFrom": "posts_block1", - "tableTo": "posts", - "columnsFrom": [ - "_parent_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "posts_block1_locales": { - "name": "posts_block1_locales", - "schema": "", - "columns": { - "localized_text": { - "name": "localized_text", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "_locale": { - "name": "_locale", - "type": "_locales", - "primaryKey": false, - "notNull": true - }, - "_parent_id": { - "name": "_parent_id", - "type": "varchar", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "posts_block1_locales__parent_id_posts_block1_id_fk": { - "name": "posts_block1_locales__parent_id_posts_block1_id_fk", - "tableFrom": "posts_block1_locales", - "tableTo": "posts_block1", - "columnsFrom": [ - "_parent_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "posts_block2_block_array": { - "name": "posts_block2_block_array", - "schema": "", - "columns": { - "_order": { - "name": "_order", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "_parent_id": { - "name": "_parent_id", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "id": { - "name": "id", - "type": "varchar", - "primaryKey": true, - "notNull": true - }, - "sub_block_array": { - "name": "sub_block_array", - "type": "varchar", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "posts_block2_block_array__parent_id_posts_block2_id_fk": { - "name": "posts_block2_block_array__parent_id_posts_block2_id_fk", - "tableFrom": "posts_block2_block_array", - "tableTo": "posts_block2", - "columnsFrom": [ - "_parent_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "posts_block2": { - "name": "posts_block2", - "schema": "", - "columns": { - "_order": { - "name": "_order", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "_path": { - "name": "_path", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "_parent_id": { - "name": "_parent_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "id": { - "name": "id", - "type": "varchar", - "primaryKey": true, - "notNull": true - }, - "number": { - "name": "number", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "block_name": { - "name": "block_name", - "type": "varchar", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "posts_block2__parent_id_posts_id_fk": { - "name": "posts_block2__parent_id_posts_id_fk", - "tableFrom": "posts_block2", - "tableTo": "posts", - "columnsFrom": [ - "_parent_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "posts_my_group_group_array": { - "name": "posts_my_group_group_array", - "schema": "", - "columns": { - "_order": { - "name": "_order", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "_parent_id": { - "name": "_parent_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "id": { - "name": "id", - "type": "varchar", - "primaryKey": true, - "notNull": true - }, - "group_array_text": { - "name": "group_array_text", - "type": "varchar", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "posts_my_group_group_array__parent_id_posts_id_fk": { - "name": "posts_my_group_group_array__parent_id_posts_id_fk", - "tableFrom": "posts_my_group_group_array", - "tableTo": "posts", - "columnsFrom": [ - "_parent_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "posts": { - "name": "posts", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "my_group_sub_field": { - "name": "my_group_sub_field", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "my_group_sub_group_sub_sub_field": { - "name": "my_group_sub_group_sub_sub_field", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "posts_locales": { - "name": "posts_locales", - "schema": "", - "columns": { - "title": { - "name": "title", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "number": { - "name": "number", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "my_group_sub_field_localized": { - "name": "my_group_sub_field_localized", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "my_group_sub_group_sub_sub_field_localized": { - "name": "my_group_sub_group_sub_sub_field_localized", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "_locale": { - "name": "_locale", - "type": "_locales", - "primaryKey": false, - "notNull": true - }, - "_parent_id": { - "name": "_parent_id", - "type": "integer", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "posts_locales__parent_id_posts_id_fk": { - "name": "posts_locales__parent_id_posts_id_fk", - "tableFrom": "posts_locales", - "tableTo": "posts", - "columnsFrom": [ - "_parent_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "posts_relationships": { - "name": "posts_relationships", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "parent_id": { - "name": "parent_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "path": { - "name": "path", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "order": { - "name": "order", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "pages_id": { - "name": "pages_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "people_id": { - "name": "people_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "posts_id": { - "name": "posts_id", - "type": "integer", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "posts_relationships_parent_id_posts_id_fk": { - "name": "posts_relationships_parent_id_posts_id_fk", - "tableFrom": "posts_relationships", - "tableTo": "posts", - "columnsFrom": [ - "parent_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "posts_relationships_pages_id_pages_id_fk": { - "name": "posts_relationships_pages_id_pages_id_fk", - "tableFrom": "posts_relationships", - "tableTo": "pages", - "columnsFrom": [ - "pages_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "posts_relationships_people_id_people_id_fk": { - "name": "posts_relationships_people_id_people_id_fk", - "tableFrom": "posts_relationships", - "tableTo": "people", - "columnsFrom": [ - "people_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "posts_relationships_posts_id_posts_id_fk": { - "name": "posts_relationships_posts_id_posts_id_fk", - "tableFrom": "posts_relationships", - "tableTo": "posts", - "columnsFrom": [ - "posts_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "pages": { - "name": "pages", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "slug": { - "name": "slug", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "people": { - "name": "people", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "full_name": { - "name": "full_name", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "email": { - "name": "email", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "reset_password_token": { - "name": "reset_password_token", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "salt": { - "name": "salt", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "hash": { - "name": "hash", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "login_attempts": { - "name": "login_attempts", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "email_idx": { - "name": "email_idx", - "columns": [ - "email" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "payload_preferences": { - "name": "payload_preferences", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "key": { - "name": "key", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "value": { - "name": "value", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "payload_preferences_relationships": { - "name": "payload_preferences_relationships", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "parent_id": { - "name": "parent_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "path": { - "name": "path", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "order": { - "name": "order", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "users_id": { - "name": "users_id", - "type": "integer", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "payload_preferences_relationships_parent_id_payload_preferences_id_fk": { - "name": "payload_preferences_relationships_parent_id_payload_preferences_id_fk", - "tableFrom": "payload_preferences_relationships", - "tableTo": "payload_preferences", - "columnsFrom": [ - "parent_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "payload_preferences_relationships_users_id_users_id_fk": { - "name": "payload_preferences_relationships_users_id_users_id_fk", - "tableFrom": "payload_preferences_relationships", - "tableTo": "users", - "columnsFrom": [ - "users_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "payload_migrations": { - "name": "payload_migrations", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "batch": { - "name": "batch", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "schema": { - "name": "schema", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - } + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} }, + "dialect": "pg", "enums": { "_locales": { "name": "_locales", @@ -926,10 +14,860 @@ } } }, + "id": "4b6f2243-b055-45d8-9afa-53cd2b729a12", + "prevId": "00000000-0000-0000-0000-000000000000", "schemas": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - } -} \ No newline at end of file + "tables": { + "pages": { + "columns": { + "created_at": { + "default": "now()", + "name": "created_at", + "notNull": true, + "primaryKey": false, + "type": "timestamp" + }, + "id": { + "name": "id", + "notNull": true, + "primaryKey": true, + "type": "serial" + }, + "slug": { + "name": "slug", + "notNull": false, + "primaryKey": false, + "type": "varchar" + }, + "updated_at": { + "default": "now()", + "name": "updated_at", + "notNull": true, + "primaryKey": false, + "type": "timestamp" + } + }, + "compositePrimaryKeys": {}, + "foreignKeys": {}, + "indexes": {}, + "name": "pages", + "schema": "", + "uniqueConstraints": {} + }, + "payload_migrations": { + "columns": { + "batch": { + "name": "batch", + "notNull": false, + "primaryKey": false, + "type": "numeric" + }, + "created_at": { + "default": "now()", + "name": "created_at", + "notNull": true, + "primaryKey": false, + "type": "timestamp" + }, + "id": { + "name": "id", + "notNull": true, + "primaryKey": true, + "type": "serial" + }, + "name": { + "name": "name", + "notNull": false, + "primaryKey": false, + "type": "varchar" + }, + "schema": { + "name": "schema", + "notNull": false, + "primaryKey": false, + "type": "jsonb" + }, + "updated_at": { + "default": "now()", + "name": "updated_at", + "notNull": true, + "primaryKey": false, + "type": "timestamp" + } + }, + "compositePrimaryKeys": {}, + "foreignKeys": {}, + "indexes": {}, + "name": "payload_migrations", + "schema": "", + "uniqueConstraints": {} + }, + "payload_preferences": { + "columns": { + "created_at": { + "default": "now()", + "name": "created_at", + "notNull": true, + "primaryKey": false, + "type": "timestamp" + }, + "id": { + "name": "id", + "notNull": true, + "primaryKey": true, + "type": "serial" + }, + "key": { + "name": "key", + "notNull": false, + "primaryKey": false, + "type": "varchar" + }, + "updated_at": { + "default": "now()", + "name": "updated_at", + "notNull": true, + "primaryKey": false, + "type": "timestamp" + }, + "value": { + "name": "value", + "notNull": false, + "primaryKey": false, + "type": "jsonb" + } + }, + "compositePrimaryKeys": {}, + "foreignKeys": {}, + "indexes": {}, + "name": "payload_preferences", + "schema": "", + "uniqueConstraints": {} + }, + "payload_preferences_relationships": { + "columns": { + "id": { + "name": "id", + "notNull": true, + "primaryKey": true, + "type": "serial" + }, + "order": { + "name": "order", + "notNull": false, + "primaryKey": false, + "type": "integer" + }, + "parent_id": { + "name": "parent_id", + "notNull": true, + "primaryKey": false, + "type": "integer" + }, + "path": { + "name": "path", + "notNull": true, + "primaryKey": false, + "type": "varchar" + }, + "users_id": { + "name": "users_id", + "notNull": false, + "primaryKey": false, + "type": "integer" + } + }, + "compositePrimaryKeys": {}, + "foreignKeys": { + "payload_preferences_relationships_parent_id_payload_preferences_id_fk": { + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "name": "payload_preferences_relationships_parent_id_payload_preferences_id_fk", + "onDelete": "no action", + "onUpdate": "no action", + "tableFrom": "payload_preferences_relationships", + "tableTo": "payload_preferences" + }, + "payload_preferences_relationships_users_id_users_id_fk": { + "columnsFrom": ["users_id"], + "columnsTo": ["id"], + "name": "payload_preferences_relationships_users_id_users_id_fk", + "onDelete": "no action", + "onUpdate": "no action", + "tableFrom": "payload_preferences_relationships", + "tableTo": "users" + } + }, + "indexes": {}, + "name": "payload_preferences_relationships", + "schema": "", + "uniqueConstraints": {} + }, + "people": { + "columns": { + "created_at": { + "default": "now()", + "name": "created_at", + "notNull": true, + "primaryKey": false, + "type": "timestamp" + }, + "full_name": { + "name": "full_name", + "notNull": false, + "primaryKey": false, + "type": "varchar" + }, + "id": { + "name": "id", + "notNull": true, + "primaryKey": true, + "type": "serial" + }, + "updated_at": { + "default": "now()", + "name": "updated_at", + "notNull": true, + "primaryKey": false, + "type": "timestamp" + } + }, + "compositePrimaryKeys": {}, + "foreignKeys": {}, + "indexes": {}, + "name": "people", + "schema": "", + "uniqueConstraints": {} + }, + "posts": { + "columns": { + "created_at": { + "default": "now()", + "name": "created_at", + "notNull": true, + "primaryKey": false, + "type": "timestamp" + }, + "id": { + "name": "id", + "notNull": true, + "primaryKey": true, + "type": "serial" + }, + "my_group_sub_field": { + "name": "my_group_sub_field", + "notNull": false, + "primaryKey": false, + "type": "varchar" + }, + "my_group_sub_group_sub_sub_field": { + "name": "my_group_sub_group_sub_sub_field", + "notNull": false, + "primaryKey": false, + "type": "varchar" + }, + "updated_at": { + "default": "now()", + "name": "updated_at", + "notNull": true, + "primaryKey": false, + "type": "timestamp" + } + }, + "compositePrimaryKeys": {}, + "foreignKeys": {}, + "indexes": {}, + "name": "posts", + "schema": "", + "uniqueConstraints": {} + }, + "posts_block1": { + "columns": { + "_order": { + "name": "_order", + "notNull": true, + "primaryKey": false, + "type": "integer" + }, + "_parent_id": { + "name": "_parent_id", + "notNull": true, + "primaryKey": false, + "type": "integer" + }, + "_path": { + "name": "_path", + "notNull": true, + "primaryKey": false, + "type": "text" + }, + "block_name": { + "name": "block_name", + "notNull": false, + "primaryKey": false, + "type": "varchar" + }, + "id": { + "name": "id", + "notNull": true, + "primaryKey": true, + "type": "varchar" + }, + "non_localized_text": { + "name": "non_localized_text", + "notNull": false, + "primaryKey": false, + "type": "varchar" + } + }, + "compositePrimaryKeys": {}, + "foreignKeys": { + "posts_block1__parent_id_posts_id_fk": { + "columnsFrom": ["_parent_id"], + "columnsTo": ["id"], + "name": "posts_block1__parent_id_posts_id_fk", + "onDelete": "no action", + "onUpdate": "no action", + "tableFrom": "posts_block1", + "tableTo": "posts" + } + }, + "indexes": {}, + "name": "posts_block1", + "schema": "", + "uniqueConstraints": {} + }, + "posts_block1_locales": { + "columns": { + "_locale": { + "name": "_locale", + "notNull": true, + "primaryKey": false, + "type": "_locales" + }, + "_parent_id": { + "name": "_parent_id", + "notNull": true, + "primaryKey": false, + "type": "varchar" + }, + "id": { + "name": "id", + "notNull": true, + "primaryKey": true, + "type": "serial" + }, + "localized_text": { + "name": "localized_text", + "notNull": false, + "primaryKey": false, + "type": "varchar" + } + }, + "compositePrimaryKeys": {}, + "foreignKeys": { + "posts_block1_locales__parent_id_posts_block1_id_fk": { + "columnsFrom": ["_parent_id"], + "columnsTo": ["id"], + "name": "posts_block1_locales__parent_id_posts_block1_id_fk", + "onDelete": "no action", + "onUpdate": "no action", + "tableFrom": "posts_block1_locales", + "tableTo": "posts_block1" + } + }, + "indexes": {}, + "name": "posts_block1_locales", + "schema": "", + "uniqueConstraints": {} + }, + "posts_block2": { + "columns": { + "_order": { + "name": "_order", + "notNull": true, + "primaryKey": false, + "type": "integer" + }, + "_parent_id": { + "name": "_parent_id", + "notNull": true, + "primaryKey": false, + "type": "integer" + }, + "_path": { + "name": "_path", + "notNull": true, + "primaryKey": false, + "type": "text" + }, + "block_name": { + "name": "block_name", + "notNull": false, + "primaryKey": false, + "type": "varchar" + }, + "id": { + "name": "id", + "notNull": true, + "primaryKey": true, + "type": "varchar" + }, + "number": { + "name": "number", + "notNull": false, + "primaryKey": false, + "type": "numeric" + } + }, + "compositePrimaryKeys": {}, + "foreignKeys": { + "posts_block2__parent_id_posts_id_fk": { + "columnsFrom": ["_parent_id"], + "columnsTo": ["id"], + "name": "posts_block2__parent_id_posts_id_fk", + "onDelete": "no action", + "onUpdate": "no action", + "tableFrom": "posts_block2", + "tableTo": "posts" + } + }, + "indexes": {}, + "name": "posts_block2", + "schema": "", + "uniqueConstraints": {} + }, + "posts_block2_block_array": { + "columns": { + "_order": { + "name": "_order", + "notNull": true, + "primaryKey": false, + "type": "integer" + }, + "_parent_id": { + "name": "_parent_id", + "notNull": true, + "primaryKey": false, + "type": "varchar" + }, + "id": { + "name": "id", + "notNull": true, + "primaryKey": true, + "type": "varchar" + }, + "sub_block_array": { + "name": "sub_block_array", + "notNull": false, + "primaryKey": false, + "type": "varchar" + } + }, + "compositePrimaryKeys": {}, + "foreignKeys": { + "posts_block2_block_array__parent_id_posts_block2_id_fk": { + "columnsFrom": ["_parent_id"], + "columnsTo": ["id"], + "name": "posts_block2_block_array__parent_id_posts_block2_id_fk", + "onDelete": "no action", + "onUpdate": "no action", + "tableFrom": "posts_block2_block_array", + "tableTo": "posts_block2" + } + }, + "indexes": {}, + "name": "posts_block2_block_array", + "schema": "", + "uniqueConstraints": {} + }, + "posts_locales": { + "columns": { + "_locale": { + "name": "_locale", + "notNull": true, + "primaryKey": false, + "type": "_locales" + }, + "_parent_id": { + "name": "_parent_id", + "notNull": true, + "primaryKey": false, + "type": "integer" + }, + "id": { + "name": "id", + "notNull": true, + "primaryKey": true, + "type": "serial" + }, + "my_group_sub_field_localized": { + "name": "my_group_sub_field_localized", + "notNull": false, + "primaryKey": false, + "type": "varchar" + }, + "my_group_sub_group_sub_sub_field_localized": { + "name": "my_group_sub_group_sub_sub_field_localized", + "notNull": false, + "primaryKey": false, + "type": "varchar" + }, + "number": { + "name": "number", + "notNull": false, + "primaryKey": false, + "type": "numeric" + }, + "title": { + "name": "title", + "notNull": false, + "primaryKey": false, + "type": "varchar" + } + }, + "compositePrimaryKeys": {}, + "foreignKeys": { + "posts_locales__parent_id_posts_id_fk": { + "columnsFrom": ["_parent_id"], + "columnsTo": ["id"], + "name": "posts_locales__parent_id_posts_id_fk", + "onDelete": "no action", + "onUpdate": "no action", + "tableFrom": "posts_locales", + "tableTo": "posts" + } + }, + "indexes": {}, + "name": "posts_locales", + "schema": "", + "uniqueConstraints": {} + }, + "posts_my_array": { + "columns": { + "_order": { + "name": "_order", + "notNull": true, + "primaryKey": false, + "type": "integer" + }, + "_parent_id": { + "name": "_parent_id", + "notNull": true, + "primaryKey": false, + "type": "integer" + }, + "id": { + "name": "id", + "notNull": true, + "primaryKey": true, + "type": "varchar" + } + }, + "compositePrimaryKeys": {}, + "foreignKeys": { + "posts_my_array__parent_id_posts_id_fk": { + "columnsFrom": ["_parent_id"], + "columnsTo": ["id"], + "name": "posts_my_array__parent_id_posts_id_fk", + "onDelete": "no action", + "onUpdate": "no action", + "tableFrom": "posts_my_array", + "tableTo": "posts" + } + }, + "indexes": {}, + "name": "posts_my_array", + "schema": "", + "uniqueConstraints": {} + }, + "posts_my_array_locales": { + "columns": { + "_locale": { + "name": "_locale", + "notNull": true, + "primaryKey": false, + "type": "_locales" + }, + "_parent_id": { + "name": "_parent_id", + "notNull": true, + "primaryKey": false, + "type": "varchar" + }, + "id": { + "name": "id", + "notNull": true, + "primaryKey": true, + "type": "serial" + }, + "sub_field": { + "name": "sub_field", + "notNull": false, + "primaryKey": false, + "type": "varchar" + } + }, + "compositePrimaryKeys": {}, + "foreignKeys": { + "posts_my_array_locales__parent_id_posts_my_array_id_fk": { + "columnsFrom": ["_parent_id"], + "columnsTo": ["id"], + "name": "posts_my_array_locales__parent_id_posts_my_array_id_fk", + "onDelete": "no action", + "onUpdate": "no action", + "tableFrom": "posts_my_array_locales", + "tableTo": "posts_my_array" + } + }, + "indexes": {}, + "name": "posts_my_array_locales", + "schema": "", + "uniqueConstraints": {} + }, + "posts_my_array_my_sub_array": { + "columns": { + "_order": { + "name": "_order", + "notNull": true, + "primaryKey": false, + "type": "integer" + }, + "_parent_id": { + "name": "_parent_id", + "notNull": true, + "primaryKey": false, + "type": "varchar" + }, + "id": { + "name": "id", + "notNull": true, + "primaryKey": true, + "type": "varchar" + }, + "sub_sub_field": { + "name": "sub_sub_field", + "notNull": false, + "primaryKey": false, + "type": "varchar" + } + }, + "compositePrimaryKeys": {}, + "foreignKeys": { + "posts_my_array_my_sub_array__parent_id_posts_my_array_id_fk": { + "columnsFrom": ["_parent_id"], + "columnsTo": ["id"], + "name": "posts_my_array_my_sub_array__parent_id_posts_my_array_id_fk", + "onDelete": "no action", + "onUpdate": "no action", + "tableFrom": "posts_my_array_my_sub_array", + "tableTo": "posts_my_array" + } + }, + "indexes": {}, + "name": "posts_my_array_my_sub_array", + "schema": "", + "uniqueConstraints": {} + }, + "posts_my_group_group_array": { + "columns": { + "_order": { + "name": "_order", + "notNull": true, + "primaryKey": false, + "type": "integer" + }, + "_parent_id": { + "name": "_parent_id", + "notNull": true, + "primaryKey": false, + "type": "integer" + }, + "group_array_text": { + "name": "group_array_text", + "notNull": false, + "primaryKey": false, + "type": "varchar" + }, + "id": { + "name": "id", + "notNull": true, + "primaryKey": true, + "type": "varchar" + } + }, + "compositePrimaryKeys": {}, + "foreignKeys": { + "posts_my_group_group_array__parent_id_posts_id_fk": { + "columnsFrom": ["_parent_id"], + "columnsTo": ["id"], + "name": "posts_my_group_group_array__parent_id_posts_id_fk", + "onDelete": "no action", + "onUpdate": "no action", + "tableFrom": "posts_my_group_group_array", + "tableTo": "posts" + } + }, + "indexes": {}, + "name": "posts_my_group_group_array", + "schema": "", + "uniqueConstraints": {} + }, + "posts_relationships": { + "columns": { + "id": { + "name": "id", + "notNull": true, + "primaryKey": true, + "type": "serial" + }, + "order": { + "name": "order", + "notNull": false, + "primaryKey": false, + "type": "integer" + }, + "pages_id": { + "name": "pages_id", + "notNull": false, + "primaryKey": false, + "type": "integer" + }, + "parent_id": { + "name": "parent_id", + "notNull": true, + "primaryKey": false, + "type": "integer" + }, + "path": { + "name": "path", + "notNull": true, + "primaryKey": false, + "type": "varchar" + }, + "people_id": { + "name": "people_id", + "notNull": false, + "primaryKey": false, + "type": "integer" + }, + "posts_id": { + "name": "posts_id", + "notNull": false, + "primaryKey": false, + "type": "integer" + } + }, + "compositePrimaryKeys": {}, + "foreignKeys": { + "posts_relationships_pages_id_pages_id_fk": { + "columnsFrom": ["pages_id"], + "columnsTo": ["id"], + "name": "posts_relationships_pages_id_pages_id_fk", + "onDelete": "no action", + "onUpdate": "no action", + "tableFrom": "posts_relationships", + "tableTo": "pages" + }, + "posts_relationships_parent_id_posts_id_fk": { + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "name": "posts_relationships_parent_id_posts_id_fk", + "onDelete": "no action", + "onUpdate": "no action", + "tableFrom": "posts_relationships", + "tableTo": "posts" + }, + "posts_relationships_people_id_people_id_fk": { + "columnsFrom": ["people_id"], + "columnsTo": ["id"], + "name": "posts_relationships_people_id_people_id_fk", + "onDelete": "no action", + "onUpdate": "no action", + "tableFrom": "posts_relationships", + "tableTo": "people" + }, + "posts_relationships_posts_id_posts_id_fk": { + "columnsFrom": ["posts_id"], + "columnsTo": ["id"], + "name": "posts_relationships_posts_id_posts_id_fk", + "onDelete": "no action", + "onUpdate": "no action", + "tableFrom": "posts_relationships", + "tableTo": "posts" + } + }, + "indexes": {}, + "name": "posts_relationships", + "schema": "", + "uniqueConstraints": {} + }, + "users": { + "columns": { + "created_at": { + "default": "now()", + "name": "created_at", + "notNull": true, + "primaryKey": false, + "type": "timestamp" + }, + "email": { + "name": "email", + "notNull": false, + "primaryKey": false, + "type": "varchar" + }, + "hash": { + "name": "hash", + "notNull": false, + "primaryKey": false, + "type": "varchar" + }, + "id": { + "name": "id", + "notNull": true, + "primaryKey": true, + "type": "serial" + }, + "login_attempts": { + "name": "login_attempts", + "notNull": false, + "primaryKey": false, + "type": "numeric" + }, + "reset_password_token": { + "name": "reset_password_token", + "notNull": false, + "primaryKey": false, + "type": "varchar" + }, + "salt": { + "name": "salt", + "notNull": false, + "primaryKey": false, + "type": "varchar" + }, + "updated_at": { + "default": "now()", + "name": "updated_at", + "notNull": true, + "primaryKey": false, + "type": "timestamp" + } + }, + "compositePrimaryKeys": {}, + "foreignKeys": {}, + "indexes": { + "email_idx": { + "columns": ["email"], + "isUnique": true, + "name": "email_idx" + } + }, + "name": "users", + "schema": "", + "uniqueConstraints": {} + } + }, + "version": "5" +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 3f3cdb2f9..6cd7d7fae 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,112 +1,112 @@ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", "configurations": [ { "command": "pnpm run dev _community", + "cwd": "${workspaceFolder}", "name": "Run Dev Community", "request": "launch", - "type": "node-terminal", - "cwd": "${workspaceFolder}" + "type": "node-terminal" }, { "command": "pnpm run dev fields", + "cwd": "${workspaceFolder}", "name": "Run Dev Fields", "request": "launch", - "type": "node-terminal", - "cwd": "${workspaceFolder}" + "type": "node-terminal" }, { "command": "pnpm run dev:postgres postgres -- -I", // Allow input + "cwd": "${workspaceFolder}", "name": "Run Dev Postgres", "request": "launch", - "type": "node-terminal", - "cwd": "${workspaceFolder}" + "type": "node-terminal" }, { "command": "pnpm run dev versions", + "cwd": "${workspaceFolder}", "name": "Run Dev Versions", "request": "launch", - "type": "node-terminal", - "cwd": "${workspaceFolder}" + "type": "node-terminal" }, { - "type": "node-terminal", "command": "ts-node src/bin/migrate.ts migrate", - "request": "launch", + "env": { + "PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts", + "PAYLOAD_DATABASE": "postgres" + // "PAYLOAD_DROP_DATABASE": "true", + }, "name": "Migrate CLI - migrate", - "env": { - "PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts", - "PAYLOAD_DATABASE": "postgres", - // "PAYLOAD_DROP_DATABASE": "true", - }, "outputCapture": "std", + "request": "launch", + "type": "node-terminal" }, { - "type": "node-terminal", "command": "ts-node src/bin/migrate.ts migrate:status", - "request": "launch", - "name": "Migrate CLI - status", "env": { "PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts", - "PAYLOAD_DATABASE": "postgres", + "PAYLOAD_DATABASE": "postgres" // "PAYLOAD_DROP_DATABASE": "true", }, + "name": "Migrate CLI - status", "outputCapture": "std", + "request": "launch", + "type": "node-terminal" }, { - "type": "node-terminal", "command": "ts-node src/bin/migrate.ts migrate:create yass", - "request": "launch", + "env": { + // "PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts", + "PAYLOAD_CONFIG_PATH": "test/postgres/config.ts", + "PAYLOAD_DATABASE": "postgres" + // "PAYLOAD_DROP_DATABASE": "true", + }, "name": "Migrate CLI - create", - "env": { - // "PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts", - "PAYLOAD_CONFIG_PATH": "test/postgres/config.ts", - "PAYLOAD_DATABASE": "postgres", - // "PAYLOAD_DROP_DATABASE": "true", - }, "outputCapture": "std", + "request": "launch", + "type": "node-terminal" }, { - "type": "node-terminal", "command": "ts-node src/bin/migrate.ts migrate:down", - "request": "launch", + "env": { + // "PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts", + "PAYLOAD_CONFIG_PATH": "test/postgres/config.ts", + "PAYLOAD_DATABASE": "postgres" + // "PAYLOAD_DROP_DATABASE": "true", + }, "name": "Migrate CLI - down", - "env": { - // "PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts", - "PAYLOAD_CONFIG_PATH": "test/postgres/config.ts", - "PAYLOAD_DATABASE": "postgres", - // "PAYLOAD_DROP_DATABASE": "true", - }, "outputCapture": "std", + "request": "launch", + "type": "node-terminal" }, { - "type": "node-terminal", "command": "ts-node src/bin/migrate.ts migrate:reset", - "request": "launch", - "name": "Migrate CLI - reset", "env": { // "PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts", "PAYLOAD_CONFIG_PATH": "test/postgres/config.ts", - "PAYLOAD_DATABASE": "postgres", + "PAYLOAD_DATABASE": "postgres" // "PAYLOAD_DROP_DATABASE": "true", }, + "name": "Migrate CLI - reset", "outputCapture": "std", + "request": "launch", + "type": "node-terminal" }, { - "type": "node-terminal", "command": "ts-node src/bin/migrate.ts migrate:refresh", - "request": "launch", - "name": "Migrate CLI - refresh", "env": { // "PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts", "PAYLOAD_CONFIG_PATH": "test/postgres/config.ts", - "PAYLOAD_DATABASE": "postgres", + "PAYLOAD_DATABASE": "postgres" // "PAYLOAD_DROP_DATABASE": "true", }, + "name": "Migrate CLI - refresh", "outputCapture": "std", - }, - ] + "request": "launch", + "type": "node-terminal" + } + ], + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 088e1af90..495fe69a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,457 +1,402 @@ - - # [1.14.0](https://github.com/payloadcms/payload/compare/v1.13.4...v1.14.0) (2023-08-16) - ### Bug Fixes -* DatePicker showing only selected day by default ([#3169](https://github.com/payloadcms/payload/issues/3169)) ([edcb393](https://github.com/payloadcms/payload/commit/edcb3933cfb4532180c822135ea6a8be928e0fdc)) -* only allow redirects to /admin sub-routes ([c0f05a1](https://github.com/payloadcms/payload/commit/c0f05a1c38fb9c958de920fabb698b5ecfb661f0)) -* passes in height to resizeOptions upload option to allow height resize ([#3171](https://github.com/payloadcms/payload/issues/3171)) ([7963d04](https://github.com/payloadcms/payload/commit/7963d04a27888eb5a12d0ab37f2082cd33638abd)) -* WhereBuilder component does not accept all valid Where queries ([#3087](https://github.com/payloadcms/payload/issues/3087)) ([fdfdfc8](https://github.com/payloadcms/payload/commit/fdfdfc83f36a958971f8e4e4f9f5e51560cb26e0)) - +- DatePicker showing only selected day by default ([#3169](https://github.com/payloadcms/payload/issues/3169)) ([edcb393](https://github.com/payloadcms/payload/commit/edcb3933cfb4532180c822135ea6a8be928e0fdc)) +- only allow redirects to /admin sub-routes ([c0f05a1](https://github.com/payloadcms/payload/commit/c0f05a1c38fb9c958de920fabb698b5ecfb661f0)) +- passes in height to resizeOptions upload option to allow height resize ([#3171](https://github.com/payloadcms/payload/issues/3171)) ([7963d04](https://github.com/payloadcms/payload/commit/7963d04a27888eb5a12d0ab37f2082cd33638abd)) +- WhereBuilder component does not accept all valid Where queries ([#3087](https://github.com/payloadcms/payload/issues/3087)) ([fdfdfc8](https://github.com/payloadcms/payload/commit/fdfdfc83f36a958971f8e4e4f9f5e51560cb26e0)) ### Features -* add afterOperation hook ([#2697](https://github.com/payloadcms/payload/issues/2697)) ([33686c6](https://github.com/payloadcms/payload/commit/33686c6db8373a16d7f6b0192e0701bf15881aa4)) -* add support for hotkeys ([#1821](https://github.com/payloadcms/payload/issues/1821)) ([942cfec](https://github.com/payloadcms/payload/commit/942cfec286ff050e13417b037cca64b9d757d868)) -* Added Azerbaijani language file ([#3164](https://github.com/payloadcms/payload/issues/3164)) ([63e3063](https://github.com/payloadcms/payload/commit/63e3063b9ecc1afd62d7a287a798d41215008f2a)) -* allow async relationship filter options ([#2951](https://github.com/payloadcms/payload/issues/2951)) ([bad3638](https://github.com/payloadcms/payload/commit/bad363882c9d00d3c73547ca3329eba988e728ff)) -* Improve admin dashboard accessibility ([#3053](https://github.com/payloadcms/payload/issues/3053)) ([e03a8e6](https://github.com/payloadcms/payload/commit/e03a8e6b030e82a17e1cdae5b4032433cf9c75a4)) -* improve field ops ([#3172](https://github.com/payloadcms/payload/issues/3172)) ([d91b44c](https://github.com/payloadcms/payload/commit/d91b44cbb3fd526caca2a6f4bd30fd06ede3a5da)) -* make PAYLOAD_CONFIG_PATH optional ([#2839](https://github.com/payloadcms/payload/issues/2839)) ([5744de7](https://github.com/payloadcms/payload/commit/5744de7ec63e3f17df7e02a7cc827818a79dbbb8)) -* text alignment for richtext editor ([#2803](https://github.com/payloadcms/payload/issues/2803)) ([a0b13a5](https://github.com/payloadcms/payload/commit/a0b13a5b01fa0d7f4c4dffd1895bfe507e5c676d)) +- add afterOperation hook ([#2697](https://github.com/payloadcms/payload/issues/2697)) ([33686c6](https://github.com/payloadcms/payload/commit/33686c6db8373a16d7f6b0192e0701bf15881aa4)) +- add support for hotkeys ([#1821](https://github.com/payloadcms/payload/issues/1821)) ([942cfec](https://github.com/payloadcms/payload/commit/942cfec286ff050e13417b037cca64b9d757d868)) +- Added Azerbaijani language file ([#3164](https://github.com/payloadcms/payload/issues/3164)) ([63e3063](https://github.com/payloadcms/payload/commit/63e3063b9ecc1afd62d7a287a798d41215008f2a)) +- allow async relationship filter options ([#2951](https://github.com/payloadcms/payload/issues/2951)) ([bad3638](https://github.com/payloadcms/payload/commit/bad363882c9d00d3c73547ca3329eba988e728ff)) +- Improve admin dashboard accessibility ([#3053](https://github.com/payloadcms/payload/issues/3053)) ([e03a8e6](https://github.com/payloadcms/payload/commit/e03a8e6b030e82a17e1cdae5b4032433cf9c75a4)) +- improve field ops ([#3172](https://github.com/payloadcms/payload/issues/3172)) ([d91b44c](https://github.com/payloadcms/payload/commit/d91b44cbb3fd526caca2a6f4bd30fd06ede3a5da)) +- make PAYLOAD_CONFIG_PATH optional ([#2839](https://github.com/payloadcms/payload/issues/2839)) ([5744de7](https://github.com/payloadcms/payload/commit/5744de7ec63e3f17df7e02a7cc827818a79dbbb8)) +- text alignment for richtext editor ([#2803](https://github.com/payloadcms/payload/issues/2803)) ([a0b13a5](https://github.com/payloadcms/payload/commit/a0b13a5b01fa0d7f4c4dffd1895bfe507e5c676d)) ## [1.13.4](https://github.com/payloadcms/payload/compare/v1.13.3...v1.13.4) (2023-08-11) - ### Bug Fixes -* correctly passes block path inside buildFieldSchemaMap ([#3162](https://github.com/payloadcms/payload/issues/3162)) ([3c60abd](https://github.com/payloadcms/payload/commit/3c60abd61aaf24d49712c80bcbd0f1113c22b85a)) +- correctly passes block path inside buildFieldSchemaMap ([#3162](https://github.com/payloadcms/payload/issues/3162)) ([3c60abd](https://github.com/payloadcms/payload/commit/3c60abd61aaf24d49712c80bcbd0f1113c22b85a)) ## [1.13.3](https://github.com/payloadcms/payload/compare/v1.13.2...v1.13.3) (2023-08-11) - ### Bug Fixes -* unable to add arrays inside secondary named tabs ([#3158](https://github.com/payloadcms/payload/issues/3158)) ([cb04d4a](https://github.com/payloadcms/payload/commit/cb04d4a82a68a764330582b93882d422b32c2527)) +- unable to add arrays inside secondary named tabs ([#3158](https://github.com/payloadcms/payload/issues/3158)) ([cb04d4a](https://github.com/payloadcms/payload/commit/cb04d4a82a68a764330582b93882d422b32c2527)) ## [1.13.2](https://github.com/payloadcms/payload/compare/v1.13.1...v1.13.2) (2023-08-10) ## [1.13.1](https://github.com/payloadcms/payload/compare/v1.13.0...v1.13.1) (2023-08-08) - ### Bug Fixes -* updates addFieldRow and replaceFieldRow rowIndex insertion ([#3145](https://github.com/payloadcms/payload/issues/3145)) ([f5cf546](https://github.com/payloadcms/payload/commit/f5cf546e1918de66998d5f0e5410bfbc1f054567)) +- updates addFieldRow and replaceFieldRow rowIndex insertion ([#3145](https://github.com/payloadcms/payload/issues/3145)) ([f5cf546](https://github.com/payloadcms/payload/commit/f5cf546e1918de66998d5f0e5410bfbc1f054567)) # [1.13.0](https://github.com/payloadcms/payload/compare/v1.12.0...v1.13.0) (2023-08-08) - ### Bug Fixes -* `setPreference()` return type ([#3125](https://github.com/payloadcms/payload/issues/3125)) ([463d6bb](https://github.com/payloadcms/payload/commit/463d6bbec66e61523bae3869df88bd98e7617390)) -* absolute staticURL admin thumbnails ([#3135](https://github.com/payloadcms/payload/issues/3135)) ([1039f39](https://github.com/payloadcms/payload/commit/1039f39c09260537616b22228080466e8df6e981)) -* adding and replacing similarly shaped block configs ([#3140](https://github.com/payloadcms/payload/issues/3140)) ([8e188cf](https://github.com/payloadcms/payload/commit/8e188cfe61db808c94d726967affdadf2e5abb9f)) - +- `setPreference()` return type ([#3125](https://github.com/payloadcms/payload/issues/3125)) ([463d6bb](https://github.com/payloadcms/payload/commit/463d6bbec66e61523bae3869df88bd98e7617390)) +- absolute staticURL admin thumbnails ([#3135](https://github.com/payloadcms/payload/issues/3135)) ([1039f39](https://github.com/payloadcms/payload/commit/1039f39c09260537616b22228080466e8df6e981)) +- adding and replacing similarly shaped block configs ([#3140](https://github.com/payloadcms/payload/issues/3140)) ([8e188cf](https://github.com/payloadcms/payload/commit/8e188cfe61db808c94d726967affdadf2e5abb9f)) ### Features -* default tab labels from name ([#3129](https://github.com/payloadcms/payload/issues/3129)) ([e8f0516](https://github.com/payloadcms/payload/commit/e8f05165eb3a28c00deb11931db01ad1f8c75c74)) -* radio and select fields are filterable by options ([#3136](https://github.com/payloadcms/payload/issues/3136)) ([b117e73](https://github.com/payloadcms/payload/commit/b117e7346434bfc8edbfa92f5db45f63c57bab08)) -* recursive saveToJWT field support ([#3130](https://github.com/payloadcms/payload/issues/3130)) ([c6e0908](https://github.com/payloadcms/payload/commit/c6e09080767dad2ab8128ba330b2b344bb25ac6f)) +- default tab labels from name ([#3129](https://github.com/payloadcms/payload/issues/3129)) ([e8f0516](https://github.com/payloadcms/payload/commit/e8f05165eb3a28c00deb11931db01ad1f8c75c74)) +- radio and select fields are filterable by options ([#3136](https://github.com/payloadcms/payload/issues/3136)) ([b117e73](https://github.com/payloadcms/payload/commit/b117e7346434bfc8edbfa92f5db45f63c57bab08)) +- recursive saveToJWT field support ([#3130](https://github.com/payloadcms/payload/issues/3130)) ([c6e0908](https://github.com/payloadcms/payload/commit/c6e09080767dad2ab8128ba330b2b344bb25ac6f)) # [1.12.0](https://github.com/payloadcms/payload/compare/v1.11.8...v1.12.0) (2023-08-04) - ### Bug Fixes -* excludes useAsTitle field from searchableFields in collection view ([#3105](https://github.com/payloadcms/payload/issues/3105)) ([8c4d251](https://github.com/payloadcms/payload/commit/8c4d2514b0f195e0059c6063346199785979c70c)) -* relationship field filter long titles ([#3113](https://github.com/payloadcms/payload/issues/3113)) ([da27a8a](https://github.com/payloadcms/payload/commit/da27a8aadbb103c5f6fe0ccc62c032876851b88f)) -* wrong links in verification and forgot password emails if serverURL not set ([#3010](https://github.com/payloadcms/payload/issues/3010)) ([6a189c6](https://github.com/payloadcms/payload/commit/6a189c6548b233aba64598af8804a56ec47e45f0)) - +- excludes useAsTitle field from searchableFields in collection view ([#3105](https://github.com/payloadcms/payload/issues/3105)) ([8c4d251](https://github.com/payloadcms/payload/commit/8c4d2514b0f195e0059c6063346199785979c70c)) +- relationship field filter long titles ([#3113](https://github.com/payloadcms/payload/issues/3113)) ([da27a8a](https://github.com/payloadcms/payload/commit/da27a8aadbb103c5f6fe0ccc62c032876851b88f)) +- wrong links in verification and forgot password emails if serverURL not set ([#3010](https://github.com/payloadcms/payload/issues/3010)) ([6a189c6](https://github.com/payloadcms/payload/commit/6a189c6548b233aba64598af8804a56ec47e45f0)) ### Features -* add support for sharp resize options ([#2844](https://github.com/payloadcms/payload/issues/2844)) ([144bb81](https://github.com/payloadcms/payload/commit/144bb81721814c19eb4957d4c8fcc845c73e2aa4)) -* allows for upload relationship drawer to be opened ([#3108](https://github.com/payloadcms/payload/issues/3108)) ([ea73e68](https://github.com/payloadcms/payload/commit/ea73e689ac46f2a7ba3b6c34e7a190944b5d5868)) -* option to pre-fill login credentials automatically ([#3021](https://github.com/payloadcms/payload/issues/3021)) ([c5756ed](https://github.com/payloadcms/payload/commit/c5756ed4a13b46bc73ae7b23309d6e9980fc81bf)) -* programmatic control over array and block rows inside the form ([#3110](https://github.com/payloadcms/payload/issues/3110)) ([a78c463](https://github.com/payloadcms/payload/commit/a78c4631b4aabb5b57448ab21ef98749b1cf1935)) -* set JWT token field name with saveToJWT ([#3126](https://github.com/payloadcms/payload/issues/3126)) ([356f174](https://github.com/payloadcms/payload/commit/356f174b9ff601facb0062d0b65db18803ef2aa2)) +- add support for sharp resize options ([#2844](https://github.com/payloadcms/payload/issues/2844)) ([144bb81](https://github.com/payloadcms/payload/commit/144bb81721814c19eb4957d4c8fcc845c73e2aa4)) +- allows for upload relationship drawer to be opened ([#3108](https://github.com/payloadcms/payload/issues/3108)) ([ea73e68](https://github.com/payloadcms/payload/commit/ea73e689ac46f2a7ba3b6c34e7a190944b5d5868)) +- option to pre-fill login credentials automatically ([#3021](https://github.com/payloadcms/payload/issues/3021)) ([c5756ed](https://github.com/payloadcms/payload/commit/c5756ed4a13b46bc73ae7b23309d6e9980fc81bf)) +- programmatic control over array and block rows inside the form ([#3110](https://github.com/payloadcms/payload/issues/3110)) ([a78c463](https://github.com/payloadcms/payload/commit/a78c4631b4aabb5b57448ab21ef98749b1cf1935)) +- set JWT token field name with saveToJWT ([#3126](https://github.com/payloadcms/payload/issues/3126)) ([356f174](https://github.com/payloadcms/payload/commit/356f174b9ff601facb0062d0b65db18803ef2aa2)) ## [1.11.8](https://github.com/payloadcms/payload/compare/v1.11.7...v1.11.8) (2023-07-31) ## [1.11.7](https://github.com/payloadcms/payload/compare/v1.11.6...v1.11.7) (2023-07-27) - ### Bug Fixes -* [#3062](https://github.com/payloadcms/payload/issues/3062) ([0280953](https://github.com/payloadcms/payload/commit/02809532b484d9018c6528cfbbbb43abfd55a540)) -* array row deletion ([#3062](https://github.com/payloadcms/payload/issues/3062)) ([cf9795b](https://github.com/payloadcms/payload/commit/cf9795b8d8b53c48335ff4c32c6c51b3de4f7bc9)) -* incorrect image rotation after being processed by sharp ([#3081](https://github.com/payloadcms/payload/issues/3081)) ([0a91950](https://github.com/payloadcms/payload/commit/0a91950f052ce40427801e6561a0f676354a2ca4)) - +- [#3062](https://github.com/payloadcms/payload/issues/3062) ([0280953](https://github.com/payloadcms/payload/commit/02809532b484d9018c6528cfbbbb43abfd55a540)) +- array row deletion ([#3062](https://github.com/payloadcms/payload/issues/3062)) ([cf9795b](https://github.com/payloadcms/payload/commit/cf9795b8d8b53c48335ff4c32c6c51b3de4f7bc9)) +- incorrect image rotation after being processed by sharp ([#3081](https://github.com/payloadcms/payload/issues/3081)) ([0a91950](https://github.com/payloadcms/payload/commit/0a91950f052ce40427801e6561a0f676354a2ca4)) ### Features -* ability to add context to payload's request object ([#2796](https://github.com/payloadcms/payload/issues/2796)) ([67ba131](https://github.com/payloadcms/payload/commit/67ba131cc61f3d3b30ef9ef7fc150344ca82da2f)) +- ability to add context to payload's request object ([#2796](https://github.com/payloadcms/payload/issues/2796)) ([67ba131](https://github.com/payloadcms/payload/commit/67ba131cc61f3d3b30ef9ef7fc150344ca82da2f)) ## [1.11.6](https://github.com/payloadcms/payload/compare/v1.11.5...v1.11.6) (2023-07-25) - ### Bug Fixes -* **collections:admin:** Enable adminThumbnail fn execution on all types ([2c74e93](https://github.com/payloadcms/payload/commit/2c74e9396a216a033e2bacdf189b7f28a0f97505)) -* threads hasMaxRows into ArrayAction components within blocks and arrays ([#3066](https://github.com/payloadcms/payload/issues/3066)) ([d43c83d](https://github.com/payloadcms/payload/commit/d43c83dad1bab5b05f4fcbae7d41de369905797c)) +- **collections:admin:** Enable adminThumbnail fn execution on all types ([2c74e93](https://github.com/payloadcms/payload/commit/2c74e9396a216a033e2bacdf189b7f28a0f97505)) +- threads hasMaxRows into ArrayAction components within blocks and arrays ([#3066](https://github.com/payloadcms/payload/issues/3066)) ([d43c83d](https://github.com/payloadcms/payload/commit/d43c83dad1bab5b05f4fcbae7d41de369905797c)) ## [1.11.5](https://github.com/payloadcms/payload/compare/v1.11.4...v1.11.5) (2023-07-25) - ### Bug Fixes -* admin route not mounting on production serve ([#3071](https://github.com/payloadcms/payload/issues/3071)) ([e718668](https://github.com/payloadcms/payload/commit/e71866856fffefcfb61dd3d29135cccb66939a62)) +- admin route not mounting on production serve ([#3071](https://github.com/payloadcms/payload/issues/3071)) ([e718668](https://github.com/payloadcms/payload/commit/e71866856fffefcfb61dd3d29135cccb66939a62)) ## [1.11.4](https://github.com/payloadcms/payload/compare/v1.11.3...v1.11.4) (2023-07-25) - ### Bug Fixes -* if arrayFieldType rows are undefined, page would crash ([#3049](https://github.com/payloadcms/payload/issues/3049)) ([08377cc](https://github.com/payloadcms/payload/commit/08377cc5a7ea9d02350177e2e1d69390ee97af78)) - +- if arrayFieldType rows are undefined, page would crash ([#3049](https://github.com/payloadcms/payload/issues/3049)) ([08377cc](https://github.com/payloadcms/payload/commit/08377cc5a7ea9d02350177e2e1d69390ee97af78)) ### Features -* bump mongoose and mongoose-paginate versions ([#3025](https://github.com/payloadcms/payload/issues/3025)) ([41d3eee](https://github.com/payloadcms/payload/commit/41d3eee35f3855798a5c3372f8ad7c742a7810f7)) -* improve keyboard focus styles ([#3011](https://github.com/payloadcms/payload/issues/3011)) ([080e619](https://github.com/payloadcms/payload/commit/080e6195ef39ec858fbb115e8f554a8dfc436438)) -* solidifies bundler adapter pattern ([#3044](https://github.com/payloadcms/payload/issues/3044)) ([641c765](https://github.com/payloadcms/payload/commit/641c765fb921e162c98f09218929348037dd0f88)) +- bump mongoose and mongoose-paginate versions ([#3025](https://github.com/payloadcms/payload/issues/3025)) ([41d3eee](https://github.com/payloadcms/payload/commit/41d3eee35f3855798a5c3372f8ad7c742a7810f7)) +- improve keyboard focus styles ([#3011](https://github.com/payloadcms/payload/issues/3011)) ([080e619](https://github.com/payloadcms/payload/commit/080e6195ef39ec858fbb115e8f554a8dfc436438)) +- solidifies bundler adapter pattern ([#3044](https://github.com/payloadcms/payload/issues/3044)) ([641c765](https://github.com/payloadcms/payload/commit/641c765fb921e162c98f09218929348037dd0f88)) ## [1.11.3](https://github.com/payloadcms/payload/compare/v1.11.2...v1.11.3) (2023-07-19) - ### Bug Fixes -* adds backdrop blur to button ([#3006](https://github.com/payloadcms/payload/issues/3006)) ([4233426](https://github.com/payloadcms/payload/commit/42334263bbc6219be92c5728f1a4ac6c8d2d1306)) -* rich text link element not validating on create ([#3014](https://github.com/payloadcms/payload/issues/3014)) ([60fca40](https://github.com/payloadcms/payload/commit/60fca40780d4ddd8e684a455de55c566ec91e223)) - +- adds backdrop blur to button ([#3006](https://github.com/payloadcms/payload/issues/3006)) ([4233426](https://github.com/payloadcms/payload/commit/42334263bbc6219be92c5728f1a4ac6c8d2d1306)) +- rich text link element not validating on create ([#3014](https://github.com/payloadcms/payload/issues/3014)) ([60fca40](https://github.com/payloadcms/payload/commit/60fca40780d4ddd8e684a455de55c566ec91e223)) ### Features -* auto-login in config capability ([#3009](https://github.com/payloadcms/payload/issues/3009)) ([733fc0b](https://github.com/payloadcms/payload/commit/733fc0b2d0cf0f2d58c8a28e84776f883774b0e0)) -* returns queried user alongside refreshed token ([#2813](https://github.com/payloadcms/payload/issues/2813)) ([2fc03f1](https://github.com/payloadcms/payload/commit/2fc03f196e4e5fa0ad3369ec976c0b6889ebda88)) -* support logger destination ([#2896](https://github.com/payloadcms/payload/issues/2896)) ([cd0bf68](https://github.com/payloadcms/payload/commit/cd0bf68a6150b1adbdb9ee318ac0a06c4476aa4d)) +- auto-login in config capability ([#3009](https://github.com/payloadcms/payload/issues/3009)) ([733fc0b](https://github.com/payloadcms/payload/commit/733fc0b2d0cf0f2d58c8a28e84776f883774b0e0)) +- returns queried user alongside refreshed token ([#2813](https://github.com/payloadcms/payload/issues/2813)) ([2fc03f1](https://github.com/payloadcms/payload/commit/2fc03f196e4e5fa0ad3369ec976c0b6889ebda88)) +- support logger destination ([#2896](https://github.com/payloadcms/payload/issues/2896)) ([cd0bf68](https://github.com/payloadcms/payload/commit/cd0bf68a6150b1adbdb9ee318ac0a06c4476aa4d)) ## [1.11.2](https://github.com/payloadcms/payload/compare/v1.11.1...v1.11.2) (2023-07-14) - ### Features -* adds array, collapsible, tab and group error states ([4925f90](https://github.com/payloadcms/payload/commit/4925f90b5f5c8fb8092bf4e8d88d5e0c1846b094)) +- adds array, collapsible, tab and group error states ([4925f90](https://github.com/payloadcms/payload/commit/4925f90b5f5c8fb8092bf4e8d88d5e0c1846b094)) ## [1.11.1](https://github.com/payloadcms/payload/compare/v1.11.0...v1.11.1) (2023-07-11) - ### Bug Fixes -* [#2980](https://github.com/payloadcms/payload/issues/2980), locale=all was not iterating through arrays / blocks ([d6bfba7](https://github.com/payloadcms/payload/commit/d6bfba72a6b1a84bc5bb9dd14c7ce31d7afcbc1c)) -* anchor Button component respect margins ([#2648](https://github.com/payloadcms/payload/issues/2648)) ([1877d22](https://github.com/payloadcms/payload/commit/1877d2247c89ca5c8e1f0e1f989154d54768fed8)) +- [#2980](https://github.com/payloadcms/payload/issues/2980), locale=all was not iterating through arrays / blocks ([d6bfba7](https://github.com/payloadcms/payload/commit/d6bfba72a6b1a84bc5bb9dd14c7ce31d7afcbc1c)) +- anchor Button component respect margins ([#2648](https://github.com/payloadcms/payload/issues/2648)) ([1877d22](https://github.com/payloadcms/payload/commit/1877d2247c89ca5c8e1f0e1f989154d54768fed8)) # [1.11.0](https://github.com/payloadcms/payload/compare/v1.10.5...v1.11.0) (2023-07-05) - ### Bug Fixes -* ensures fields within blocks respect field level access control ([#2969](https://github.com/payloadcms/payload/issues/2969)) ([5b79067](https://github.com/payloadcms/payload/commit/5b79067cc14874abbd1e1a5b6e619d41571b187f)) -* ensures rows always have id's ([#2968](https://github.com/payloadcms/payload/issues/2968)) ([04851d0](https://github.com/payloadcms/payload/commit/04851d0dc99e4a3df0a1ac642e9a4b9a3c06d8a1)) -* GraphQL type for number field ([#2954](https://github.com/payloadcms/payload/issues/2954)) ([29d8bf0](https://github.com/payloadcms/payload/commit/29d8bf0927038d2305218e5a6b811e0c4039d617)) -* nested richtext bug and test ([#2966](https://github.com/payloadcms/payload/issues/2966)) ([801f609](https://github.com/payloadcms/payload/commit/801f60939b1bb4e33fbabe1f9a3c4a04a47912db)) -* properly threads custom react-select props through relationship field ([#2973](https://github.com/payloadcms/payload/issues/2973)) ([79393e8](https://github.com/payloadcms/payload/commit/79393e8cf0b79b31fa711536e0bc22b1a251468a)) - +- ensures fields within blocks respect field level access control ([#2969](https://github.com/payloadcms/payload/issues/2969)) ([5b79067](https://github.com/payloadcms/payload/commit/5b79067cc14874abbd1e1a5b6e619d41571b187f)) +- ensures rows always have id's ([#2968](https://github.com/payloadcms/payload/issues/2968)) ([04851d0](https://github.com/payloadcms/payload/commit/04851d0dc99e4a3df0a1ac642e9a4b9a3c06d8a1)) +- GraphQL type for number field ([#2954](https://github.com/payloadcms/payload/issues/2954)) ([29d8bf0](https://github.com/payloadcms/payload/commit/29d8bf0927038d2305218e5a6b811e0c4039d617)) +- nested richtext bug and test ([#2966](https://github.com/payloadcms/payload/issues/2966)) ([801f609](https://github.com/payloadcms/payload/commit/801f60939b1bb4e33fbabe1f9a3c4a04a47912db)) +- properly threads custom react-select props through relationship field ([#2973](https://github.com/payloadcms/payload/issues/2973)) ([79393e8](https://github.com/payloadcms/payload/commit/79393e8cf0b79b31fa711536e0bc22b1a251468a)) ### Features -* improve typing of ExtendableError and APIError ([#2864](https://github.com/payloadcms/payload/issues/2864)) ([7c47e4b](https://github.com/payloadcms/payload/commit/7c47e4b0d3c63f6f7800daaf424935d6067ffcc4)) -* narrow endpoint.method type ([#1880](https://github.com/payloadcms/payload/issues/1880)) ([b734a1c](https://github.com/payloadcms/payload/commit/b734a1c422d200cad1085b7e92f8540df4238e32)) +- improve typing of ExtendableError and APIError ([#2864](https://github.com/payloadcms/payload/issues/2864)) ([7c47e4b](https://github.com/payloadcms/payload/commit/7c47e4b0d3c63f6f7800daaf424935d6067ffcc4)) +- narrow endpoint.method type ([#1880](https://github.com/payloadcms/payload/issues/1880)) ([b734a1c](https://github.com/payloadcms/payload/commit/b734a1c422d200cad1085b7e92f8540df4238e32)) ## [1.10.5](https://github.com/payloadcms/payload/compare/v1.10.4...v1.10.5) (2023-06-30) - ### Bug Fixes -* fields in drawer cannot be edited ([#2949](https://github.com/payloadcms/payload/issues/2949)) ([0c2e41c](https://github.com/payloadcms/payload/commit/0c2e41c4bef9333c47a9b1db0de807696b3f3872)), closes [#2945](https://github.com/payloadcms/payload/issues/2945) -* improve versions test suite ([#2941](https://github.com/payloadcms/payload/issues/2941)) ([1d4df99](https://github.com/payloadcms/payload/commit/1d4df99ea78c5f682074ae824dcd8dea18b774e0)) -* incorrect graphql type generation ([#2898](https://github.com/payloadcms/payload/issues/2898)) ([b36deb4](https://github.com/payloadcms/payload/commit/b36deb4640cad4f494a12ab74b4e4d9a918cd94b)) +- fields in drawer cannot be edited ([#2949](https://github.com/payloadcms/payload/issues/2949)) ([0c2e41c](https://github.com/payloadcms/payload/commit/0c2e41c4bef9333c47a9b1db0de807696b3f3872)), closes [#2945](https://github.com/payloadcms/payload/issues/2945) +- improve versions test suite ([#2941](https://github.com/payloadcms/payload/issues/2941)) ([1d4df99](https://github.com/payloadcms/payload/commit/1d4df99ea78c5f682074ae824dcd8dea18b774e0)) +- incorrect graphql type generation ([#2898](https://github.com/payloadcms/payload/issues/2898)) ([b36deb4](https://github.com/payloadcms/payload/commit/b36deb4640cad4f494a12ab74b4e4d9a918cd94b)) ## [1.10.4](https://github.com/payloadcms/payload/compare/v1.10.3...v1.10.4) (2023-06-30) - ### Features -* add locale to displayed API URL ([b22d157](https://github.com/payloadcms/payload/commit/b22d157bd2f1c1a857e2d42bdc5b893549e3db9e)) +- add locale to displayed API URL ([b22d157](https://github.com/payloadcms/payload/commit/b22d157bd2f1c1a857e2d42bdc5b893549e3db9e)) ## [1.10.3](https://github.com/payloadcms/payload/compare/v1.10.2...v1.10.3) (2023-06-30) - ### Bug Fixes -* [#2937](https://github.com/payloadcms/payload/issues/2937), depth not being respected in graphql rich text fields ([f84b432](https://github.com/payloadcms/payload/commit/f84b4323e2fce57e2e14b181e486ed72cc09ded5)) -* shows updatedAt date when selecting a version to compare from dropdown ([3c9dab3](https://github.com/payloadcms/payload/commit/3c9dab3b9d5302d8bdf5792f0384cd5aeeb13839)) +- [#2937](https://github.com/payloadcms/payload/issues/2937), depth not being respected in graphql rich text fields ([f84b432](https://github.com/payloadcms/payload/commit/f84b4323e2fce57e2e14b181e486ed72cc09ded5)) +- shows updatedAt date when selecting a version to compare from dropdown ([3c9dab3](https://github.com/payloadcms/payload/commit/3c9dab3b9d5302d8bdf5792f0384cd5aeeb13839)) ## [1.10.2](https://github.com/payloadcms/payload/compare/v1.10.1...v1.10.2) (2023-06-26) - ### Bug Fixes -* adjusts swc loader to only exclude non ts/tsx files - [#2888](https://github.com/payloadcms/payload/issues/2888) ([#2907](https://github.com/payloadcms/payload/issues/2907)) ([a2d9ef3](https://github.com/payloadcms/payload/commit/a2d9ef3ca618934df58102a7e02e86dbe0ed63da)) -* autosave on localized fields, adds test ([6893231](https://github.com/payloadcms/payload/commit/6893231f85f702189089a6d78d3f3af63aaa0d82)) -* broken export of entityToJSONSchema ([#2894](https://github.com/payloadcms/payload/issues/2894)) ([837dccc](https://github.com/payloadcms/payload/commit/837dcccefeffe7bb6e674713b4184c4eb92db8dc)) -* correctly scopes data variable within bulk update - [#2901](https://github.com/payloadcms/payload/issues/2901) ([#2904](https://github.com/payloadcms/payload/issues/2904)) ([f627277](https://github.com/payloadcms/payload/commit/f627277479e6a4a847e79f54c545712a7186abb9)) -* safely check for tempFilePath when updating media document ([#2899](https://github.com/payloadcms/payload/issues/2899)) ([8206c0f](https://github.com/payloadcms/payload/commit/8206c0fe8be78a5e0f7c8e64996d73d135b1fcc2)) +- adjusts swc loader to only exclude non ts/tsx files - [#2888](https://github.com/payloadcms/payload/issues/2888) ([#2907](https://github.com/payloadcms/payload/issues/2907)) ([a2d9ef3](https://github.com/payloadcms/payload/commit/a2d9ef3ca618934df58102a7e02e86dbe0ed63da)) +- autosave on localized fields, adds test ([6893231](https://github.com/payloadcms/payload/commit/6893231f85f702189089a6d78d3f3af63aaa0d82)) +- broken export of entityToJSONSchema ([#2894](https://github.com/payloadcms/payload/issues/2894)) ([837dccc](https://github.com/payloadcms/payload/commit/837dcccefeffe7bb6e674713b4184c4eb92db8dc)) +- correctly scopes data variable within bulk update - [#2901](https://github.com/payloadcms/payload/issues/2901) ([#2904](https://github.com/payloadcms/payload/issues/2904)) ([f627277](https://github.com/payloadcms/payload/commit/f627277479e6a4a847e79f54c545712a7186abb9)) +- safely check for tempFilePath when updating media document ([#2899](https://github.com/payloadcms/payload/issues/2899)) ([8206c0f](https://github.com/payloadcms/payload/commit/8206c0fe8be78a5e0f7c8e64996d73d135b1fcc2)) ## [1.10.1](https://github.com/payloadcms/payload/compare/v1.10.0...v1.10.1) (2023-06-22) - ### Bug Fixes -* conditional fields perf bug - [#2886](https://github.com/payloadcms/payload/issues/2886) ([#2890](https://github.com/payloadcms/payload/issues/2890)) ([b83d788](https://github.com/payloadcms/payload/commit/b83d788d3cfe12f87dcd63a9df20b939a6f4681e)) -* cutoff tooltips in relationship field ([#2873](https://github.com/payloadcms/payload/issues/2873)) ([09c6cad](https://github.com/payloadcms/payload/commit/09c6cad3e8462dc3d8b1b6424aafd336c1d7828c)) -* Relationship hasMany and filterOptions fails above 10 items ([#2891](https://github.com/payloadcms/payload/issues/2891)) ([8128de6](https://github.com/payloadcms/payload/commit/8128de64dff98fdbcf053faef9de3c3f9a733071)) +- conditional fields perf bug - [#2886](https://github.com/payloadcms/payload/issues/2886) ([#2890](https://github.com/payloadcms/payload/issues/2890)) ([b83d788](https://github.com/payloadcms/payload/commit/b83d788d3cfe12f87dcd63a9df20b939a6f4681e)) +- cutoff tooltips in relationship field ([#2873](https://github.com/payloadcms/payload/issues/2873)) ([09c6cad](https://github.com/payloadcms/payload/commit/09c6cad3e8462dc3d8b1b6424aafd336c1d7828c)) +- Relationship hasMany and filterOptions fails above 10 items ([#2891](https://github.com/payloadcms/payload/issues/2891)) ([8128de6](https://github.com/payloadcms/payload/commit/8128de64dff98fdbcf053faef9de3c3f9a733071)) # [1.10.0](https://github.com/payloadcms/payload/compare/v1.9.5...v1.10.0) (2023-06-20) - ### Bug Fixes -* [#2831](https://github.com/payloadcms/payload/issues/2831), persists payloadAPI through local operations that accept req ([85d2467](https://github.com/payloadcms/payload/commit/85d2467d73582a372ee34e3ce93403847a1f0689)) -* [#2842](https://github.com/payloadcms/payload/issues/2842), querying number custom ids with in ([116e9ff](https://github.com/payloadcms/payload/commit/116e9ffe81f44c4b40fa578b4a8fe4bb70fd110c)) -* default sort with near operator ([#2862](https://github.com/payloadcms/payload/issues/2862)) ([99f3809](https://github.com/payloadcms/payload/commit/99f38098dd4a386437c469becc975ca86c54601f)) -* deprecate min/max in exchange for minRows and maxRows for relationship field ([#2826](https://github.com/payloadcms/payload/issues/2826)) ([0d8d7f3](https://github.com/payloadcms/payload/commit/0d8d7f358d390184f6f888d77858b4a145e94214)) -* drawer close on backspace ([#2869](https://github.com/payloadcms/payload/issues/2869)) ([a110ba2](https://github.com/payloadcms/payload/commit/a110ba2dc09cd0824a9b1eb8e011604388277bd8)) -* drawer fields are read-only if opened from a hasMany relationship ([#2843](https://github.com/payloadcms/payload/issues/2843)) ([542b536](https://github.com/payloadcms/payload/commit/542b5362d3ec8741aff6b1672fab7d2250e7b854)) -* fields in relationship drawer not usable [#2815](https://github.com/payloadcms/payload/issues/2815) ([#2870](https://github.com/payloadcms/payload/issues/2870)) ([8626dc6](https://github.com/payloadcms/payload/commit/8626dc6b1a926143e7ba505f3edd924432168675)) -* mobile loading overlay width [#2866](https://github.com/payloadcms/payload/issues/2866) ([#2867](https://github.com/payloadcms/payload/issues/2867)) ([ba9d633](https://github.com/payloadcms/payload/commit/ba9d6336acc779cfec0db312c8e2da912ce58cd4)) -* near query sorting by distance and pagination ([#2861](https://github.com/payloadcms/payload/issues/2861)) ([1611896](https://github.com/payloadcms/payload/commit/16118960aa6d63f7a429f168ff4305f336b1b1e6)) -* relationship field query pagination ([#2871](https://github.com/payloadcms/payload/issues/2871)) ([ce84174](https://github.com/payloadcms/payload/commit/ce84174554d9d828cbaaaa9548e5defc0feb4e2b)) -* slow like queries with lots of records ([4dd703a](https://github.com/payloadcms/payload/commit/4dd703a6bff0ab7d06af234baa975553bd62f176)) - +- [#2831](https://github.com/payloadcms/payload/issues/2831), persists payloadAPI through local operations that accept req ([85d2467](https://github.com/payloadcms/payload/commit/85d2467d73582a372ee34e3ce93403847a1f0689)) +- [#2842](https://github.com/payloadcms/payload/issues/2842), querying number custom ids with in ([116e9ff](https://github.com/payloadcms/payload/commit/116e9ffe81f44c4b40fa578b4a8fe4bb70fd110c)) +- default sort with near operator ([#2862](https://github.com/payloadcms/payload/issues/2862)) ([99f3809](https://github.com/payloadcms/payload/commit/99f38098dd4a386437c469becc975ca86c54601f)) +- deprecate min/max in exchange for minRows and maxRows for relationship field ([#2826](https://github.com/payloadcms/payload/issues/2826)) ([0d8d7f3](https://github.com/payloadcms/payload/commit/0d8d7f358d390184f6f888d77858b4a145e94214)) +- drawer close on backspace ([#2869](https://github.com/payloadcms/payload/issues/2869)) ([a110ba2](https://github.com/payloadcms/payload/commit/a110ba2dc09cd0824a9b1eb8e011604388277bd8)) +- drawer fields are read-only if opened from a hasMany relationship ([#2843](https://github.com/payloadcms/payload/issues/2843)) ([542b536](https://github.com/payloadcms/payload/commit/542b5362d3ec8741aff6b1672fab7d2250e7b854)) +- fields in relationship drawer not usable [#2815](https://github.com/payloadcms/payload/issues/2815) ([#2870](https://github.com/payloadcms/payload/issues/2870)) ([8626dc6](https://github.com/payloadcms/payload/commit/8626dc6b1a926143e7ba505f3edd924432168675)) +- mobile loading overlay width [#2866](https://github.com/payloadcms/payload/issues/2866) ([#2867](https://github.com/payloadcms/payload/issues/2867)) ([ba9d633](https://github.com/payloadcms/payload/commit/ba9d6336acc779cfec0db312c8e2da912ce58cd4)) +- near query sorting by distance and pagination ([#2861](https://github.com/payloadcms/payload/issues/2861)) ([1611896](https://github.com/payloadcms/payload/commit/16118960aa6d63f7a429f168ff4305f336b1b1e6)) +- relationship field query pagination ([#2871](https://github.com/payloadcms/payload/issues/2871)) ([ce84174](https://github.com/payloadcms/payload/commit/ce84174554d9d828cbaaaa9548e5defc0feb4e2b)) +- slow like queries with lots of records ([4dd703a](https://github.com/payloadcms/payload/commit/4dd703a6bff0ab7d06af234baa975553bd62f176)) ### Features -* automatically redirect a user back to their originally requested URL after login ([#2838](https://github.com/payloadcms/payload/issues/2838)) ([e910688](https://github.com/payloadcms/payload/commit/e9106882f721d43bcc05a1690bda7754b450404e)) -* hasMany for number field ([#2517](https://github.com/payloadcms/payload/issues/2517)) ([8f086e3](https://github.com/payloadcms/payload/commit/8f086e315cb30be9d399fd3022c16952fb81cb2e)), closes [#2812](https://github.com/payloadcms/payload/issues/2812) [#2821](https://github.com/payloadcms/payload/issues/2821) [#2823](https://github.com/payloadcms/payload/issues/2823) [#2824](https://github.com/payloadcms/payload/issues/2824) [#2814](https://github.com/payloadcms/payload/issues/2814) [#2793](https://github.com/payloadcms/payload/issues/2793) [#2835](https://github.com/payloadcms/payload/issues/2835) -* optimizes conditional logic performance ([967f217](https://github.com/payloadcms/payload/commit/967f21734600de1fec8c1227a354ef5a417e54c5)) +- automatically redirect a user back to their originally requested URL after login ([#2838](https://github.com/payloadcms/payload/issues/2838)) ([e910688](https://github.com/payloadcms/payload/commit/e9106882f721d43bcc05a1690bda7754b450404e)) +- hasMany for number field ([#2517](https://github.com/payloadcms/payload/issues/2517)) ([8f086e3](https://github.com/payloadcms/payload/commit/8f086e315cb30be9d399fd3022c16952fb81cb2e)), closes [#2812](https://github.com/payloadcms/payload/issues/2812) [#2821](https://github.com/payloadcms/payload/issues/2821) [#2823](https://github.com/payloadcms/payload/issues/2823) [#2824](https://github.com/payloadcms/payload/issues/2824) [#2814](https://github.com/payloadcms/payload/issues/2814) [#2793](https://github.com/payloadcms/payload/issues/2793) [#2835](https://github.com/payloadcms/payload/issues/2835) +- optimizes conditional logic performance ([967f217](https://github.com/payloadcms/payload/commit/967f21734600de1fec8c1227a354ef5a417e54c5)) ## [1.9.5](https://github.com/payloadcms/payload/compare/v1.9.4...v1.9.5) (2023-06-16) ## [1.9.4](https://github.com/payloadcms/payload/compare/v1.9.3...v1.9.4) (2023-06-16) - ### Bug Fixes -* incorrectly return totalDocs=1 instead of the correct count when pagination=false ([2e73938](https://github.com/payloadcms/payload/commit/2e7393853447d2da41ddef79f73e9026719a674b)) +- incorrectly return totalDocs=1 instead of the correct count when pagination=false ([2e73938](https://github.com/payloadcms/payload/commit/2e7393853447d2da41ddef79f73e9026719a674b)) ## [1.9.3](https://github.com/payloadcms/payload/compare/v1.9.2...v1.9.3) (2023-06-16) - ### Bug Fixes -* adds custom property to ui field in joi validation ([#2835](https://github.com/payloadcms/payload/issues/2835)) ([56d7745](https://github.com/payloadcms/payload/commit/56d7745139e31c5d42c5191477f409f12589a952)) -* ensures relations to object ids can be queried on ([c3d6e1b](https://github.com/payloadcms/payload/commit/c3d6e1b490a69f0aadb00e54e46a8774732e6658)) +- adds custom property to ui field in joi validation ([#2835](https://github.com/payloadcms/payload/issues/2835)) ([56d7745](https://github.com/payloadcms/payload/commit/56d7745139e31c5d42c5191477f409f12589a952)) +- ensures relations to object ids can be queried on ([c3d6e1b](https://github.com/payloadcms/payload/commit/c3d6e1b490a69f0aadb00e54e46a8774732e6658)) ## [1.9.2](https://github.com/payloadcms/payload/compare/v1.9.1...v1.9.2) (2023-06-14) - ### Bug Fixes -* [#2821](https://github.com/payloadcms/payload/issues/2821) i18n ui field label ([#2823](https://github.com/payloadcms/payload/issues/2823)) ([63cd7fb](https://github.com/payloadcms/payload/commit/63cd7fbd0c91bbf5120e95fd33388a38e593b341)) -* adds missing dark-mode styles for version differences view ([#2812](https://github.com/payloadcms/payload/issues/2812)) ([346a48f](https://github.com/payloadcms/payload/commit/346a48f871e09a3d5e25b7ff9e45689a104b0f9f)) -* sanitize reset password result - [#2805](https://github.com/payloadcms/payload/issues/2805) ([#2808](https://github.com/payloadcms/payload/issues/2808)) ([46a5f41](https://github.com/payloadcms/payload/commit/46a5f417217313b049f4b412abb3319634f27262)) -* user can be created without having to specify an email - [#2801](https://github.com/payloadcms/payload/issues/2801) ([abe3852](https://github.com/payloadcms/payload/commit/abe38520aaaefdfaea4c47130eea04a42a82627b)) +- [#2821](https://github.com/payloadcms/payload/issues/2821) i18n ui field label ([#2823](https://github.com/payloadcms/payload/issues/2823)) ([63cd7fb](https://github.com/payloadcms/payload/commit/63cd7fbd0c91bbf5120e95fd33388a38e593b341)) +- adds missing dark-mode styles for version differences view ([#2812](https://github.com/payloadcms/payload/issues/2812)) ([346a48f](https://github.com/payloadcms/payload/commit/346a48f871e09a3d5e25b7ff9e45689a104b0f9f)) +- sanitize reset password result - [#2805](https://github.com/payloadcms/payload/issues/2805) ([#2808](https://github.com/payloadcms/payload/issues/2808)) ([46a5f41](https://github.com/payloadcms/payload/commit/46a5f417217313b049f4b412abb3319634f27262)) +- user can be created without having to specify an email - [#2801](https://github.com/payloadcms/payload/issues/2801) ([abe3852](https://github.com/payloadcms/payload/commit/abe38520aaaefdfaea4c47130eea04a42a82627b)) ## [1.9.1](https://github.com/payloadcms/payload/compare/v1.9.0...v1.9.1) (2023-06-09) - ### Features -* adds option to customize filename on upload ([596eea1](https://github.com/payloadcms/payload/commit/596eea1f0a42628464e5269c496360b808c35f97)) -* collection list view custom components: BeforeList, BeforeListTable, AfterListTable, AfterList ([#2792](https://github.com/payloadcms/payload/issues/2792)) ([38e962f](https://github.com/payloadcms/payload/commit/38e962f2cbcaf9eaa72276969289efdbf670c7c7)) +- adds option to customize filename on upload ([596eea1](https://github.com/payloadcms/payload/commit/596eea1f0a42628464e5269c496360b808c35f97)) +- collection list view custom components: BeforeList, BeforeListTable, AfterListTable, AfterList ([#2792](https://github.com/payloadcms/payload/issues/2792)) ([38e962f](https://github.com/payloadcms/payload/commit/38e962f2cbcaf9eaa72276969289efdbf670c7c7)) # [1.9.0](https://github.com/payloadcms/payload/compare/v1.8.6...v1.9.0) (2023-06-07) - ### Features -* custom type interfaces ([#2709](https://github.com/payloadcms/payload/issues/2709)) ([8458a98](https://github.com/payloadcms/payload/commit/8458a98eff0eedf1abfd9ec065a084955a9b8149)) +- custom type interfaces ([#2709](https://github.com/payloadcms/payload/issues/2709)) ([8458a98](https://github.com/payloadcms/payload/commit/8458a98eff0eedf1abfd9ec065a084955a9b8149)) ## [1.8.6](https://github.com/payloadcms/payload/compare/v1.8.5...v1.8.6) (2023-06-07) - ### Bug Fixes -* [#2711](https://github.com/payloadcms/payload/issues/2711) index sortable field global versions fields ([#2775](https://github.com/payloadcms/payload/issues/2775)) ([576af01](https://github.com/payloadcms/payload/commit/576af01b6f81d24621d522e8d8b9c496eafa6df0)) -* [#2767](https://github.com/payloadcms/payload/issues/2767) bulk operations missing locales in admin requests ([e30871a](https://github.com/payloadcms/payload/commit/e30871a96ff25f12401a3cc3bc5e12c064eeff3f)) -* [#2771](https://github.com/payloadcms/payload/issues/2771) relationship field not querying all collections ([#2774](https://github.com/payloadcms/payload/issues/2774)) ([8b767a1](https://github.com/payloadcms/payload/commit/8b767a166aa16659d8880cc68da546251725b20b)) -* adjusts activation constraint of draggable nodes ([#2773](https://github.com/payloadcms/payload/issues/2773)) ([863be3d](https://github.com/payloadcms/payload/commit/863be3d852af6c6a76021695f895badf23e776ae)) -* flattens relationships in the update operation for globals [#2766](https://github.com/payloadcms/payload/issues/2766) ([#2776](https://github.com/payloadcms/payload/issues/2776)) ([3677cf6](https://github.com/payloadcms/payload/commit/3677cf688d0e456c42068b4eab0086e64407d938)) -* improperly typing optional arrays with required fields as required ([f1fc305](https://github.com/payloadcms/payload/commit/f1fc305ac443ecb247622bc89067b129e96146fc)) -* read-only Auth fields ([#2781](https://github.com/payloadcms/payload/issues/2781)) ([3c72f33](https://github.com/payloadcms/payload/commit/3c72f3303c57e88256266c343225157e0b081bba)) -* read-only Auth fields ([#2781](https://github.com/payloadcms/payload/issues/2781)) ([60f5522](https://github.com/payloadcms/payload/commit/60f5522e67acb353e6d5ce05f0012241c192d4b4)) -* recursiveNestedPaths not merging existing fields when hoisting row/collapsible fields ([#2769](https://github.com/payloadcms/payload/issues/2769)) ([536d701](https://github.com/payloadcms/payload/commit/536d7017eebd5a8e14b2936c55a7fccc90d3f530)) +- [#2711](https://github.com/payloadcms/payload/issues/2711) index sortable field global versions fields ([#2775](https://github.com/payloadcms/payload/issues/2775)) ([576af01](https://github.com/payloadcms/payload/commit/576af01b6f81d24621d522e8d8b9c496eafa6df0)) +- [#2767](https://github.com/payloadcms/payload/issues/2767) bulk operations missing locales in admin requests ([e30871a](https://github.com/payloadcms/payload/commit/e30871a96ff25f12401a3cc3bc5e12c064eeff3f)) +- [#2771](https://github.com/payloadcms/payload/issues/2771) relationship field not querying all collections ([#2774](https://github.com/payloadcms/payload/issues/2774)) ([8b767a1](https://github.com/payloadcms/payload/commit/8b767a166aa16659d8880cc68da546251725b20b)) +- adjusts activation constraint of draggable nodes ([#2773](https://github.com/payloadcms/payload/issues/2773)) ([863be3d](https://github.com/payloadcms/payload/commit/863be3d852af6c6a76021695f895badf23e776ae)) +- flattens relationships in the update operation for globals [#2766](https://github.com/payloadcms/payload/issues/2766) ([#2776](https://github.com/payloadcms/payload/issues/2776)) ([3677cf6](https://github.com/payloadcms/payload/commit/3677cf688d0e456c42068b4eab0086e64407d938)) +- improperly typing optional arrays with required fields as required ([f1fc305](https://github.com/payloadcms/payload/commit/f1fc305ac443ecb247622bc89067b129e96146fc)) +- read-only Auth fields ([#2781](https://github.com/payloadcms/payload/issues/2781)) ([3c72f33](https://github.com/payloadcms/payload/commit/3c72f3303c57e88256266c343225157e0b081bba)) +- read-only Auth fields ([#2781](https://github.com/payloadcms/payload/issues/2781)) ([60f5522](https://github.com/payloadcms/payload/commit/60f5522e67acb353e6d5ce05f0012241c192d4b4)) +- recursiveNestedPaths not merging existing fields when hoisting row/collapsible fields ([#2769](https://github.com/payloadcms/payload/issues/2769)) ([536d701](https://github.com/payloadcms/payload/commit/536d7017eebd5a8e14b2936c55a7fccc90d3f530)) ## [1.8.5](https://github.com/payloadcms/payload/compare/v1.8.4...v1.8.5) (2023-06-03) - ### Features -* allows objectid through relationship validation ([42afa6b](https://github.com/payloadcms/payload/commit/42afa6b48aa924fa0dfc9defadf08ddb029da6c1)) +- allows objectid through relationship validation ([42afa6b](https://github.com/payloadcms/payload/commit/42afa6b48aa924fa0dfc9defadf08ddb029da6c1)) ## [1.8.4](https://github.com/payloadcms/payload/compare/v1.8.3...v1.8.4) (2023-06-02) - ### Features -* Add Bulgarian translation ([#2753](https://github.com/payloadcms/payload/issues/2753)) ([51108c0](https://github.com/payloadcms/payload/commit/51108c02ea346fd41c1b94ef7c339feec8383dd1)) - +- Add Bulgarian translation ([#2753](https://github.com/payloadcms/payload/issues/2753)) ([51108c0](https://github.com/payloadcms/payload/commit/51108c02ea346fd41c1b94ef7c339feec8383dd1)) ### Bug Fixes -* group row hoisting ([#2683](https://github.com/payloadcms/payload/issues/2683)) ([1626e17](https://github.com/payloadcms/payload/commit/1626e173b7eced83c59e8eb4f70b0bb68fdb0e7a)) -* graphql where types on rows and collapsible's ([#2758](https://github.com/payloadcms/payload/issues/2758)) ([f978299](https://github.com/payloadcms/payload/commit/f978299868bf352e147070afdf556bf1153bac56)) -* RichText link custom fields ([#2756](https://github.com/payloadcms/payload/issues/2756)) ([23be263](https://github.com/payloadcms/payload/commit/23be263dd2e75dca448019b1c66d7f6dd3558b37)) -* adds timestamps to global schemas ([#2738](https://github.com/payloadcms/payload/issues/2738)) ([0986282](https://github.com/payloadcms/payload/commit/0986282f13d8a3b5596c4a241b4da35e6fac6aa1)) -* adjusts code field joi schema to allow editorOptions ([ed136fb](https://github.com/payloadcms/payload/commit/ed136fbc5146889cd30c641d4947da58b66dfb2f)) -* fix locale popup overflow ([#2737](https://github.com/payloadcms/payload/issues/2737)) ([8ee9724](https://github.com/payloadcms/payload/commit/8ee9724277d419de78b27a8ffa22f3a599361251)) -* fix tests by hard-coding the URL in the logger ([2697974](https://github.com/payloadcms/payload/commit/2697974694112440bf1737c4ce535ba77bf4b194)) -* mongoose connection ([#2754](https://github.com/payloadcms/payload/issues/2754)) ([69b97bb](https://github.com/payloadcms/payload/commit/69b97bbc590c62fffbcd03a42f0e9737e3f7ca01)) -* removes payload dependency inception ([#2717](https://github.com/payloadcms/payload/issues/2717)) ([6125b66](https://github.com/payloadcms/payload/commit/6125b66286e5315725ca0ae365c81a04c1c1a54c)) -* searches on correct useAsTitle field in polymorphic list drawers [#2710](https://github.com/payloadcms/payload/issues/2710) ([9ec2a40](https://github.com/payloadcms/payload/commit/9ec2a40274ea9b3a32e43cb992df3897baf62e63)) -* typing of sendMail function ([e3ff4c4](https://github.com/payloadcms/payload/commit/e3ff4c46cbecf731c9a3c688682bcb33012cb234)) -* corrects relationship field schema from pr [#2696](https://github.com/payloadcms/payload/issues/2696) ([#2714](https://github.com/payloadcms/payload/issues/2714)) ([8285bac](https://github.com/payloadcms/payload/commit/8285bac2f5eb443b6af160b21726edf3f828a52f)) - +- group row hoisting ([#2683](https://github.com/payloadcms/payload/issues/2683)) ([1626e17](https://github.com/payloadcms/payload/commit/1626e173b7eced83c59e8eb4f70b0bb68fdb0e7a)) +- graphql where types on rows and collapsible's ([#2758](https://github.com/payloadcms/payload/issues/2758)) ([f978299](https://github.com/payloadcms/payload/commit/f978299868bf352e147070afdf556bf1153bac56)) +- RichText link custom fields ([#2756](https://github.com/payloadcms/payload/issues/2756)) ([23be263](https://github.com/payloadcms/payload/commit/23be263dd2e75dca448019b1c66d7f6dd3558b37)) +- adds timestamps to global schemas ([#2738](https://github.com/payloadcms/payload/issues/2738)) ([0986282](https://github.com/payloadcms/payload/commit/0986282f13d8a3b5596c4a241b4da35e6fac6aa1)) +- adjusts code field joi schema to allow editorOptions ([ed136fb](https://github.com/payloadcms/payload/commit/ed136fbc5146889cd30c641d4947da58b66dfb2f)) +- fix locale popup overflow ([#2737](https://github.com/payloadcms/payload/issues/2737)) ([8ee9724](https://github.com/payloadcms/payload/commit/8ee9724277d419de78b27a8ffa22f3a599361251)) +- fix tests by hard-coding the URL in the logger ([2697974](https://github.com/payloadcms/payload/commit/2697974694112440bf1737c4ce535ba77bf4b194)) +- mongoose connection ([#2754](https://github.com/payloadcms/payload/issues/2754)) ([69b97bb](https://github.com/payloadcms/payload/commit/69b97bbc590c62fffbcd03a42f0e9737e3f7ca01)) +- removes payload dependency inception ([#2717](https://github.com/payloadcms/payload/issues/2717)) ([6125b66](https://github.com/payloadcms/payload/commit/6125b66286e5315725ca0ae365c81a04c1c1a54c)) +- searches on correct useAsTitle field in polymorphic list drawers [#2710](https://github.com/payloadcms/payload/issues/2710) ([9ec2a40](https://github.com/payloadcms/payload/commit/9ec2a40274ea9b3a32e43cb992df3897baf62e63)) +- typing of sendMail function ([e3ff4c4](https://github.com/payloadcms/payload/commit/e3ff4c46cbecf731c9a3c688682bcb33012cb234)) +- corrects relationship field schema from pr [#2696](https://github.com/payloadcms/payload/issues/2696) ([#2714](https://github.com/payloadcms/payload/issues/2714)) ([8285bac](https://github.com/payloadcms/payload/commit/8285bac2f5eb443b6af160b21726edf3f828a52f)) ## [1.8.3](https://github.com/payloadcms/payload/compare/v1.8.3...v1.8.3) (2023-05-24) - ### Bug Fixes -* [#2662](https://github.com/payloadcms/payload/issues/2662), draft=true querying by id ([3b78ab0](https://github.com/payloadcms/payload/commit/3b78ab04c7a68e39afa9936ac692169ed2c8fb74)) -* [#2685](https://github.com/payloadcms/payload/issues/2685), graphql querying relationships with custom id ([9bb5470](https://github.com/payloadcms/payload/commit/9bb54703423b3f0fdb242a5e63f322d346323b06)) -* adds credentials to doc access request ([#2705](https://github.com/payloadcms/payload/issues/2705)) ([c716954](https://github.com/payloadcms/payload/commit/c716954e89b0aef976cbcbef9ece981ec9bab233)) -* prevents add new relationship modal from adding duplicative values to the parent doc [#2688](https://github.com/payloadcms/payload/issues/2688) ([a2a8ac9](https://github.com/payloadcms/payload/commit/a2a8ac9549bd67e6ab578772689684fd2bc64872)) -* unable to clear relationships or open relationship drawer on mobile [#2691](https://github.com/payloadcms/payload/issues/2691) [#2692](https://github.com/payloadcms/payload/issues/2692) ([782f8ca](https://github.com/payloadcms/payload/commit/782f8ca047178cadb4214702854a0e0cb2d9eaab)) +- [#2662](https://github.com/payloadcms/payload/issues/2662), draft=true querying by id ([3b78ab0](https://github.com/payloadcms/payload/commit/3b78ab04c7a68e39afa9936ac692169ed2c8fb74)) +- [#2685](https://github.com/payloadcms/payload/issues/2685), graphql querying relationships with custom id ([9bb5470](https://github.com/payloadcms/payload/commit/9bb54703423b3f0fdb242a5e63f322d346323b06)) +- adds credentials to doc access request ([#2705](https://github.com/payloadcms/payload/issues/2705)) ([c716954](https://github.com/payloadcms/payload/commit/c716954e89b0aef976cbcbef9ece981ec9bab233)) +- prevents add new relationship modal from adding duplicative values to the parent doc [#2688](https://github.com/payloadcms/payload/issues/2688) ([a2a8ac9](https://github.com/payloadcms/payload/commit/a2a8ac9549bd67e6ab578772689684fd2bc64872)) +- unable to clear relationships or open relationship drawer on mobile [#2691](https://github.com/payloadcms/payload/issues/2691) [#2692](https://github.com/payloadcms/payload/issues/2692) ([782f8ca](https://github.com/payloadcms/payload/commit/782f8ca047178cadb4214702854a0e0cb2d9eaab)) ## [1.8.2](https://github.com/payloadcms/payload/compare/v1.8.1...v1.8.2) (2023-05-10) - ### Bug Fixes -* react webpack alias ([1732bb8](https://github.com/payloadcms/payload/commit/1732bb877ca9688fc87cf44fbf63d05b6be23de2)) +- react webpack alias ([1732bb8](https://github.com/payloadcms/payload/commit/1732bb877ca9688fc87cf44fbf63d05b6be23de2)) ## [1.8.1](https://github.com/payloadcms/payload/compare/v1.8.0...v1.8.1) (2023-05-10) - ### Bug Fixes -* add dotenv.config() to test/dev.ts ([#2646](https://github.com/payloadcms/payload/issues/2646)) ([7963e75](https://github.com/payloadcms/payload/commit/7963e7540f4899c16a49b47cf5145f46ea0c71cf)) - +- add dotenv.config() to test/dev.ts ([#2646](https://github.com/payloadcms/payload/issues/2646)) ([7963e75](https://github.com/payloadcms/payload/commit/7963e7540f4899c16a49b47cf5145f46ea0c71cf)) ### Features -* allow users to manipulate images without needing to resize them ([#2574](https://github.com/payloadcms/payload/issues/2574)) ([8531687](https://github.com/payloadcms/payload/commit/85316879cd97933ed34588b0cee72798964de281)) -* export additional graphql types ([#2610](https://github.com/payloadcms/payload/issues/2610)) ([3f185cb](https://github.com/payloadcms/payload/commit/3f185cb18b9677654b92921267ffef408388d0d1)) +- allow users to manipulate images without needing to resize them ([#2574](https://github.com/payloadcms/payload/issues/2574)) ([8531687](https://github.com/payloadcms/payload/commit/85316879cd97933ed34588b0cee72798964de281)) +- export additional graphql types ([#2610](https://github.com/payloadcms/payload/issues/2610)) ([3f185cb](https://github.com/payloadcms/payload/commit/3f185cb18b9677654b92921267ffef408388d0d1)) # [1.8.0](https://github.com/payloadcms/payload/compare/v1.7.5...v1.8.0) (2023-05-09) - ### Bug Fixes -* correct casing on graphql type ([219f50b](https://github.com/payloadcms/payload/commit/219f50b0bc7a520655a5ae4f1d8b08fd04c8a3dd)) -* defaultValue missing from Upload field schema ([7b21eaf](https://github.com/payloadcms/payload/commit/7b21eaf12da64778568b45e56fa8d39e81f11c29)) -* ensures nested querying works when querying across collections ([09974fa](https://github.com/payloadcms/payload/commit/09974fa68677586c727943cc234311f87bf6da75)) -* query custom text id fields ([967f2ac](https://github.com/payloadcms/payload/commit/967f2ace0ea1a65570f69e85920f2f55626efde0)) -* removes deprecated queryHiddenFIelds from local API docs ([5f30dbb](https://github.com/payloadcms/payload/commit/5f30dbb1a5b7c7ab6752c114710f92c159319d3d)) -* removes queryHiddenFields from example Find operation ([fb4f822](https://github.com/payloadcms/payload/commit/fb4f822d34d0235a537f96515073e2662680412f)) -* resolve process/browser package in webpack config ([02f27f3](https://github.com/payloadcms/payload/commit/02f27f3de6fdaf5dd0023298fc671a8ae9a1b758)) -* Row groups in tabs vertical alignment ([#2593](https://github.com/payloadcms/payload/issues/2593)) ([54fac4a](https://github.com/payloadcms/payload/commit/54fac4a5d793b534e25600d2f9470c449f40df1d)) -* softens columns and filters pill colors ([#2642](https://github.com/payloadcms/payload/issues/2642)) ([9072096](https://github.com/payloadcms/payload/commit/90720964953d392d85982052b3a4843a5450681e)) -* webp upload formatting ([ccd6ca2](https://github.com/payloadcms/payload/commit/ccd6ca298e69faf04709535df3fcb18eb3d40f1b)) - +- correct casing on graphql type ([219f50b](https://github.com/payloadcms/payload/commit/219f50b0bc7a520655a5ae4f1d8b08fd04c8a3dd)) +- defaultValue missing from Upload field schema ([7b21eaf](https://github.com/payloadcms/payload/commit/7b21eaf12da64778568b45e56fa8d39e81f11c29)) +- ensures nested querying works when querying across collections ([09974fa](https://github.com/payloadcms/payload/commit/09974fa68677586c727943cc234311f87bf6da75)) +- query custom text id fields ([967f2ac](https://github.com/payloadcms/payload/commit/967f2ace0ea1a65570f69e85920f2f55626efde0)) +- removes deprecated queryHiddenFIelds from local API docs ([5f30dbb](https://github.com/payloadcms/payload/commit/5f30dbb1a5b7c7ab6752c114710f92c159319d3d)) +- removes queryHiddenFields from example Find operation ([fb4f822](https://github.com/payloadcms/payload/commit/fb4f822d34d0235a537f96515073e2662680412f)) +- resolve process/browser package in webpack config ([02f27f3](https://github.com/payloadcms/payload/commit/02f27f3de6fdaf5dd0023298fc671a8ae9a1b758)) +- Row groups in tabs vertical alignment ([#2593](https://github.com/payloadcms/payload/issues/2593)) ([54fac4a](https://github.com/payloadcms/payload/commit/54fac4a5d793b534e25600d2f9470c449f40df1d)) +- softens columns and filters pill colors ([#2642](https://github.com/payloadcms/payload/issues/2642)) ([9072096](https://github.com/payloadcms/payload/commit/90720964953d392d85982052b3a4843a5450681e)) +- webp upload formatting ([ccd6ca2](https://github.com/payloadcms/payload/commit/ccd6ca298e69faf04709535df3fcb18eb3d40f1b)) ### Features -* add Arabic translations ([#2641](https://github.com/payloadcms/payload/issues/2641)) ([7d04cf1](https://github.com/payloadcms/payload/commit/7d04cf14fb0587f2208745bb77ed4fd17e99c8d5)) -* allow full URL in staticURL ([#2562](https://github.com/payloadcms/payload/issues/2562)) ([a9b5dff](https://github.com/payloadcms/payload/commit/a9b5dffa00623eb48302d51b88c3449920c10f46)) +- add Arabic translations ([#2641](https://github.com/payloadcms/payload/issues/2641)) ([7d04cf1](https://github.com/payloadcms/payload/commit/7d04cf14fb0587f2208745bb77ed4fd17e99c8d5)) +- allow full URL in staticURL ([#2562](https://github.com/payloadcms/payload/issues/2562)) ([a9b5dff](https://github.com/payloadcms/payload/commit/a9b5dffa00623eb48302d51b88c3449920c10f46)) ## [1.7.5](https://github.com/payloadcms/payload/compare/v1.7.4...v1.7.5) (2023-05-04) - ### Bug Fixes -* make incrementName match multiple digits ([#2609](https://github.com/payloadcms/payload/issues/2609)) ([8dbf0a2](https://github.com/payloadcms/payload/commit/8dbf0a2bd88db1b361ce16bb730613de489f2ed2)) - +- make incrementName match multiple digits ([#2609](https://github.com/payloadcms/payload/issues/2609)) ([8dbf0a2](https://github.com/payloadcms/payload/commit/8dbf0a2bd88db1b361ce16bb730613de489f2ed2)) ### Features -* collection admin.enableRichTextLink property ([#2560](https://github.com/payloadcms/payload/issues/2560)) ([9678992](https://github.com/payloadcms/payload/commit/967899229f458d06a3931d086bcc49299dc310b7)) -* custom admin buttons ([#2618](https://github.com/payloadcms/payload/issues/2618)) ([1d58007](https://github.com/payloadcms/payload/commit/1d58007606fa7e34007f2a56a3ca653d2cd3404d)) +- collection admin.enableRichTextLink property ([#2560](https://github.com/payloadcms/payload/issues/2560)) ([9678992](https://github.com/payloadcms/payload/commit/967899229f458d06a3931d086bcc49299dc310b7)) +- custom admin buttons ([#2618](https://github.com/payloadcms/payload/issues/2618)) ([1d58007](https://github.com/payloadcms/payload/commit/1d58007606fa7e34007f2a56a3ca653d2cd3404d)) ## [1.7.4](https://github.com/payloadcms/payload/compare/v1.7.3...v1.7.4) (2023-05-02) - ### Bug Fixes -* properly import SwcMinifyWebpackPlugin ([#2600](https://github.com/payloadcms/payload/issues/2600)) ([802deac](https://github.com/payloadcms/payload/commit/802deaca03f8506fa4a7adb8fc008205c2c4f013)) +- properly import SwcMinifyWebpackPlugin ([#2600](https://github.com/payloadcms/payload/issues/2600)) ([802deac](https://github.com/payloadcms/payload/commit/802deaca03f8506fa4a7adb8fc008205c2c4f013)) ## [1.7.3](https://github.com/payloadcms/payload/compare/v1.7.2...v1.7.3) (2023-05-01) - ### Bug Fixes -* [#2592](https://github.com/payloadcms/payload/issues/2592), allows usage of hidden fields within access query constraints ([#2599](https://github.com/payloadcms/payload/issues/2599)) ([a0bb13a](https://github.com/payloadcms/payload/commit/a0bb13a4123b51d770b364ddaee3dde1c5a3da53)) -* addds workaround for slate isBlock function issue ([#2596](https://github.com/payloadcms/payload/issues/2596)) ([8f6f13d](https://github.com/payloadcms/payload/commit/8f6f13dc93f49f5ba5384a9168ced5baec85e1fb)) -* bulk operations result type ([#2588](https://github.com/payloadcms/payload/issues/2588)) ([8382faa](https://github.com/payloadcms/payload/commit/8382faa0afc8118f4fb873c657a52c48abb2a6ad)) -* query on id throws 500 ([#2587](https://github.com/payloadcms/payload/issues/2587)) ([0ba22c3](https://github.com/payloadcms/payload/commit/0ba22c3aafca67be78814357edc668ed11ec4a97)) -* timestamp queries ([#2583](https://github.com/payloadcms/payload/issues/2583)) ([9c5107e](https://github.com/payloadcms/payload/commit/9c5107e86d70e36ac181c9d3ad51edacf9fc529a)) - +- [#2592](https://github.com/payloadcms/payload/issues/2592), allows usage of hidden fields within access query constraints ([#2599](https://github.com/payloadcms/payload/issues/2599)) ([a0bb13a](https://github.com/payloadcms/payload/commit/a0bb13a4123b51d770b364ddaee3dde1c5a3da53)) +- addds workaround for slate isBlock function issue ([#2596](https://github.com/payloadcms/payload/issues/2596)) ([8f6f13d](https://github.com/payloadcms/payload/commit/8f6f13dc93f49f5ba5384a9168ced5baec85e1fb)) +- bulk operations result type ([#2588](https://github.com/payloadcms/payload/issues/2588)) ([8382faa](https://github.com/payloadcms/payload/commit/8382faa0afc8118f4fb873c657a52c48abb2a6ad)) +- query on id throws 500 ([#2587](https://github.com/payloadcms/payload/issues/2587)) ([0ba22c3](https://github.com/payloadcms/payload/commit/0ba22c3aafca67be78814357edc668ed11ec4a97)) +- timestamp queries ([#2583](https://github.com/payloadcms/payload/issues/2583)) ([9c5107e](https://github.com/payloadcms/payload/commit/9c5107e86d70e36ac181c9d3ad51edacf9fc529a)) ### Features -* Add new translation for romanian language ([#2556](https://github.com/payloadcms/payload/issues/2556)) ([fbf3a2a](https://github.com/payloadcms/payload/commit/fbf3a2a1b4633e704e467d9aec05f3ae0b900bae)) -* add persian translations ([#2553](https://github.com/payloadcms/payload/issues/2553)) ([c80f68a](https://github.com/payloadcms/payload/commit/c80f68af943c730996c9cdad87cf84d4d06a5777)) -* adjust stack trace for api error ([#2598](https://github.com/payloadcms/payload/issues/2598)) ([870838e](https://github.com/payloadcms/payload/commit/870838e7563b6767c53f4dc0288119087e3f9486)) -* allow customizing the link fields ([#2559](https://github.com/payloadcms/payload/issues/2559)) ([bf65228](https://github.com/payloadcms/payload/commit/bf6522898db353e75db11525ea5a1b58243333d8)) -* supports collection compound indexes ([#2529](https://github.com/payloadcms/payload/issues/2529)) ([85b3d57](https://github.com/payloadcms/payload/commit/85b3d579d3054aad2de793957cf6454332361327)) +- Add new translation for romanian language ([#2556](https://github.com/payloadcms/payload/issues/2556)) ([fbf3a2a](https://github.com/payloadcms/payload/commit/fbf3a2a1b4633e704e467d9aec05f3ae0b900bae)) +- add persian translations ([#2553](https://github.com/payloadcms/payload/issues/2553)) ([c80f68a](https://github.com/payloadcms/payload/commit/c80f68af943c730996c9cdad87cf84d4d06a5777)) +- adjust stack trace for api error ([#2598](https://github.com/payloadcms/payload/issues/2598)) ([870838e](https://github.com/payloadcms/payload/commit/870838e7563b6767c53f4dc0288119087e3f9486)) +- allow customizing the link fields ([#2559](https://github.com/payloadcms/payload/issues/2559)) ([bf65228](https://github.com/payloadcms/payload/commit/bf6522898db353e75db11525ea5a1b58243333d8)) +- supports collection compound indexes ([#2529](https://github.com/payloadcms/payload/issues/2529)) ([85b3d57](https://github.com/payloadcms/payload/commit/85b3d579d3054aad2de793957cf6454332361327)) ## [1.7.2](https://github.com/payloadcms/payload/compare/v1.7.1...v1.7.2) (2023-04-25) - ### Bug Fixes -* [#2521](https://github.com/payloadcms/payload/issues/2521), graphql AND not working with drafts ([e67ca20](https://github.com/payloadcms/payload/commit/e67ca2010831c14938d3f639fcb5374ca62747ba)) -* document drawer access control [#2545](https://github.com/payloadcms/payload/issues/2545) ([439caf8](https://github.com/payloadcms/payload/commit/439caf815fc99538f14b3a59835dcf49185759dc)) -* prevent floating point number in image sizes ([#1935](https://github.com/payloadcms/payload/issues/1935)) ([7fcde11](https://github.com/payloadcms/payload/commit/7fcde11fa0b232537de606e44c0af68b122daed2)) -* prevent sharp toFormat settings fallthrough by using clone ([#2547](https://github.com/payloadcms/payload/issues/2547)) ([90dab3c](https://github.com/payloadcms/payload/commit/90dab3c445d4bdbab0eff286a2b66861d04f2a93)) -* query localized fields without localization configured ([12edb1c](https://github.com/payloadcms/payload/commit/12edb1cc4b2675d9b0948fb7f3439f61c6e2015d)) -* read-only styles ([823d022](https://github.com/payloadcms/payload/commit/823d0228c949fe58a7e0f11f95354b240c3ea876)) - +- [#2521](https://github.com/payloadcms/payload/issues/2521), graphql AND not working with drafts ([e67ca20](https://github.com/payloadcms/payload/commit/e67ca2010831c14938d3f639fcb5374ca62747ba)) +- document drawer access control [#2545](https://github.com/payloadcms/payload/issues/2545) ([439caf8](https://github.com/payloadcms/payload/commit/439caf815fc99538f14b3a59835dcf49185759dc)) +- prevent floating point number in image sizes ([#1935](https://github.com/payloadcms/payload/issues/1935)) ([7fcde11](https://github.com/payloadcms/payload/commit/7fcde11fa0b232537de606e44c0af68b122daed2)) +- prevent sharp toFormat settings fallthrough by using clone ([#2547](https://github.com/payloadcms/payload/issues/2547)) ([90dab3c](https://github.com/payloadcms/payload/commit/90dab3c445d4bdbab0eff286a2b66861d04f2a93)) +- query localized fields without localization configured ([12edb1c](https://github.com/payloadcms/payload/commit/12edb1cc4b2675d9b0948fb7f3439f61c6e2015d)) +- read-only styles ([823d022](https://github.com/payloadcms/payload/commit/823d0228c949fe58a7e0f11f95354b240c3ea876)) ### Features -* add rich-text blockquote element, change quote node type to blockquote ([ed230a4](https://github.com/payloadcms/payload/commit/ed230a42e0315dc2492b4a26e3bf8b5334e89380)) -* add user to field conditional logic ([274edc7](https://github.com/payloadcms/payload/commit/274edc74a70202e8c771c5111507b585c3f69377)) -* exposes id in conditional logic ([c117b32](https://github.com/payloadcms/payload/commit/c117b321474b8318c3a0ddf544e49568e461f0d8)) -* **imageresizer:** add trim options ([#2073](https://github.com/payloadcms/payload/issues/2073)) ([0406548](https://github.com/payloadcms/payload/commit/0406548fe6127e091db9926ee42e59f9158eff5a)) +- add rich-text blockquote element, change quote node type to blockquote ([ed230a4](https://github.com/payloadcms/payload/commit/ed230a42e0315dc2492b4a26e3bf8b5334e89380)) +- add user to field conditional logic ([274edc7](https://github.com/payloadcms/payload/commit/274edc74a70202e8c771c5111507b585c3f69377)) +- exposes id in conditional logic ([c117b32](https://github.com/payloadcms/payload/commit/c117b321474b8318c3a0ddf544e49568e461f0d8)) +- **imageresizer:** add trim options ([#2073](https://github.com/payloadcms/payload/issues/2073)) ([0406548](https://github.com/payloadcms/payload/commit/0406548fe6127e091db9926ee42e59f9158eff5a)) ## [1.7.1](https://github.com/payloadcms/payload/compare/v1.7.0...v1.7.1) (2023-04-18) - ### Bug Fixes -* adds 'use client' for next 13 compatibility ([5e02985](https://github.com/payloadcms/payload/commit/5e029852060d6475eccada35ffbcdd0178d5e690)) -* graphql variables not being passed properly ([72be80a](https://github.com/payloadcms/payload/commit/72be80abc4082013e052aef1152a5de749a6f3c4)) - +- adds 'use client' for next 13 compatibility ([5e02985](https://github.com/payloadcms/payload/commit/5e029852060d6475eccada35ffbcdd0178d5e690)) +- graphql variables not being passed properly ([72be80a](https://github.com/payloadcms/payload/commit/72be80abc4082013e052aef1152a5de749a6f3c4)) ### Features -* configuration extension points ([023719d](https://github.com/payloadcms/payload/commit/023719d77554a70493d779ba94bf55058d4caf98)) +- configuration extension points ([023719d](https://github.com/payloadcms/payload/commit/023719d77554a70493d779ba94bf55058d4caf98)) ## [1.7.0](https://github.com/payloadcms/payload/compare/v1.6.32...v1.7.0) (2023-04-17) @@ -829,47 +774,43 @@ We are pulling off a bandaid here and enforcing that `payload.init` is now async To migrate, you need to convert your code everywhere that you run `payload.init` to be asynchronous instead. For example, here is an example of a traditional `payload.init` call which needs to be migrated: ```js -const express = require("express"); -const payload = require("payload"); +const express = require('express') +const payload = require('payload') -const app = express(); +const app = express() payload.init({ - secret: "SECRET_KEY", - mongoURL: "mongodb://localhost/payload", + secret: 'SECRET_KEY', + mongoURL: 'mongodb://localhost/payload', express: app, -}); +}) app.listen(3000, async () => { - console.log( - "Express is now listening for incoming connections on port 3000." - ); -}); + console.log('Express is now listening for incoming connections on port 3000.') +}) ``` Your `payload.init` call will need to be converted into the following: ```js -const express = require("express"); -const payload = require("payload"); +const express = require('express') +const payload = require('payload') -const app = express(); +const app = express() const start = async () => { await payload.init({ - secret: "SECRET_KEY", - mongoURL: "mongodb://localhost/payload", + secret: 'SECRET_KEY', + mongoURL: 'mongodb://localhost/payload', express: app, - }); + }) app.listen(3000, async () => { - console.log( - "Express is now listening for incoming connections on port 3000." - ); - }); -}; + console.log('Express is now listening for incoming connections on port 3000.') + }) +} -start(); +start() ``` Notice that all we've done is wrapped the `payload.init` and `app.listen` calls with a `start` function that is asynchronous. @@ -880,18 +821,18 @@ Before this release, the Local API methods were configured as generics. For exam ```ts const post = await payload.findByID({ - collection: "posts", - id: "id-of-post-here", -}); + collection: 'posts', + id: 'id-of-post-here', +}) ``` Now, you don't need to pass your types and Payload will automatically infer them for you, as well as significantly improve typing throughout the local API. Here's an example: ```ts const post = await payload.findByID({ - collection: "posts", // this is now auto-typed - id: "id-of-post-here", -}); + collection: 'posts', // this is now auto-typed + id: 'id-of-post-here', +}) // `post` will be automatically typed as `Post` ``` @@ -938,11 +879,11 @@ To migrate, create this file within the root of your Payload project: **migrateVersions.ts** ```ts -const payload = require("payload"); +const payload = require('payload') -require("dotenv").config(); +require('dotenv').config() -const { PAYLOAD_SECRET, MONGODB_URI } = process.env; +const { PAYLOAD_SECRET, MONGODB_URI } = process.env // This function ensures that there is at least one corresponding version for any document // within each of your draft-enabled collections. @@ -955,7 +896,7 @@ const ensureAtLeastOneVersion = async () => { secret: PAYLOAD_SECRET, mongoURL: MONGODB_URI, local: true, - }); + }) // For each collection await Promise.all( @@ -966,14 +907,14 @@ const ensureAtLeastOneVersion = async () => { collection: slug, limit: 0, depth: 0, - locale: "all", - }); + locale: 'all', + }) - const VersionsModel = payload.versions[slug]; - const existingCollectionDocIds: Array = []; + const VersionsModel = payload.versions[slug] + const existingCollectionDocIds: Array = [] await Promise.all( docs.map(async (doc) => { - existingCollectionDocIds.push(doc.id); + existingCollectionDocIds.push(doc.id) // Find at least one version for the doc const versionDocs = await VersionsModel.find( { @@ -981,8 +922,8 @@ const ensureAtLeastOneVersion = async () => { updatedAt: { $gte: doc.updatedAt }, }, null, - { limit: 1 } - ).lean(); + { limit: 1 }, + ).lean() // If there are no corresponding versions, // we need to create one @@ -994,39 +935,37 @@ const ensureAtLeastOneVersion = async () => { autosave: Boolean(versions?.drafts?.autosave), updatedAt: doc.updatedAt, createdAt: doc.createdAt, - }); + }) } catch (e) { console.error( `Unable to create version corresponding with collection ${slug} document ID ${doc.id}`, - e?.errors || e - ); + e?.errors || e, + ) } - console.log( - `Created version corresponding with ${slug} document ID ${doc.id}` - ); + console.log(`Created version corresponding with ${slug} document ID ${doc.id}`) } - }) - ); + }), + ) const versionsWithoutParentDocs = await VersionsModel.deleteMany({ parent: { $nin: existingCollectionDocIds }, - }); + }) if (versionsWithoutParentDocs.deletedCount > 0) { console.log( - `Removing ${versionsWithoutParentDocs.deletedCount} versions for ${slug} collection - parent documents no longer exist` - ); + `Removing ${versionsWithoutParentDocs.deletedCount} versions for ${slug} collection - parent documents no longer exist`, + ) } } - }) - ); + }), + ) - console.log("Done!"); - process.exit(0); -}; + console.log('Done!') + process.exit(0) +} -ensureAtLeastOneVersion(); +ensureAtLeastOneVersion() ``` Make sure your environment variables match the script's values above and then run `PAYLOAD_CONFIG_PATH=src/payload.config.ts npx ts-node -T migrateVersions.ts` in your terminal. Make sure that you point the command to your Payload config. @@ -1368,32 +1307,32 @@ Any future slugs after updating will be used as-is. // Before const ExampleCollection: CollectionConfig = { - slug: "case-studies", + slug: 'case-studies', labels: { // Before Payload used `labels.singular` to generate types/graphQL schema - singular: "Project", - plural: "Projects", + singular: 'Project', + plural: 'Projects', }, - }; + } // After const ExampleCollection: CollectionConfig = { // Now Payload uses `slug` to generate types/graphQL schema - slug: "case-studies", + slug: 'case-studies', labels: { - singular: "Project", - plural: "Projects", + singular: 'Project', + plural: 'Projects', }, // To override the usage of slug in graphQL schema generation graphQL: { - singularName: "Project", - pluralName: "Projects", + singularName: 'Project', + pluralName: 'Projects', }, // To override the usage of slug in type file generation typescript: { - interface: "Project", + interface: 'Project', }, - }; + } ``` - **Globals:** are affected if you have a `label` defined that differs from your global slug. @@ -1403,25 +1342,25 @@ Any future slugs after updating will be used as-is. // Before const ExampleGlobal: GlobalConfig = { - slug: "footer", + slug: 'footer', // Before Payload used `label` to generate types/graphQL schema - label: "Page Footer", - }; + label: 'Page Footer', + } // After const ExampleGlobal: GlobalConfig = { // Now Payload uses `slug` to generate types/graphQL schema - slug: "footer", - label: "Page Footer", + slug: 'footer', + label: 'Page Footer', // To override the usage of slug in graphQL schema generation graphQL: { - name: "PageFooter", + name: 'PageFooter', }, // To override the usage of slug in type file generation typescript: { - interface: "PageFooter", + interface: 'PageFooter', }, - }; + } ``` - **Block Fields:** are affected if you have a `label` defined that differs from your block slug. @@ -2694,25 +2633,25 @@ Now, configs will be sanitized **_before_** plugins are executed **_as well as_* So, where your plugin may have been typed like this before: ```ts -import { SanitizedConfig } from "payload/config"; +import { SanitizedConfig } from 'payload/config' const plugin = (config: SanitizedConfig): SanitizedConfig => { return { ...config, - }; -}; + } +} ``` It can now be written like this: ```ts -import { Config } from "payload/config"; +import { Config } from 'payload/config' const plugin = (config: Config): Config => { return { ...config, - }; -}; + } +} ``` ### Features @@ -2944,24 +2883,24 @@ For example, if you have a `pages` collection with no existing access control, a ```js const Page = { - slug: "pages", + slug: 'pages', access: { // No `read` access control was set }, -}; +} ``` To: ```js const Page = { - slug: "pages", + slug: 'pages', access: { // Now we explicitly allow public read access // to this collection's documents read: () => true, }, -}; +} ``` If none of your collections or globals should be publicly exposed, you don't need to do anything to upgrade. @@ -3522,4 +3461,4 @@ If none of your collections or globals should be publicly exposed, you don't nee - add blind index for encrypting API Keys ([9a1c1f6](https://github.com/payloadcms/payload/commit/9a1c1f64c0ea0066b679195f50e6cb1ac4bf3552)) - add license key to access routej ([2565005](https://github.com/payloadcms/payload/commit/2565005cc099797a6e3b8995e0984c28b7837e82)) -## [0.0.137](https://github.com/payloadcms/payload/commit/5c1e2846a2694a80cc8707703406c2ac1bb6af8a) (2020-11-12) \ No newline at end of file +## [0.0.137](https://github.com/payloadcms/payload/commit/5c1e2846a2694a80cc8707703406c2ac1bb6af8a) (2020-11-12) diff --git a/ISSUE_GUIDE.md b/ISSUE_GUIDE.md index 680bdccfb..d64fec0e9 100644 --- a/ISSUE_GUIDE.md +++ b/ISSUE_GUIDE.md @@ -6,9 +6,10 @@ To report an issue, please follow the steps below: 2. Add necessary collections/globals/fields to the `test/_community` directory to recreate the issue you are experiencing 3. Create an issue and add a link to your forked repo -**The goal is to isolate the problem by reducing the number of fields/collections you add to the test/_community folder. This folder is not meant for you to copy your project into, but to recreate the issue you are experiencing with minimal config.** +**The goal is to isolate the problem by reducing the number of fields/collections you add to the test/\_community folder. This folder is not meant for you to copy your project into, but to recreate the issue you are experiencing with minimal config.** ## Test directory file tree explanation + ```text . ├── config.ts @@ -25,19 +26,21 @@ To report an issue, please follow the steps below: The directory split up in this way specifically to reduce friction when creating tests and to add the ability to boot up Payload with that specific config. You should modify the files in `test/_community` to get started. ## How to start test collection admin UI + To start the admin panel so you can manually recreate your issue, you can run the following command: - ```bash - # This command will start up Payload using your config - # NOTE: it will wipe the test database on restart - pnpm dev _community - ``` - +```bash +# This command will start up Payload using your config +# NOTE: it will wipe the test database on restart +pnpm dev _community +``` ## Testing is optional but encouraged + An issue does not need to have failing tests — reproduction steps with your forked repo are enough at this point. Some people like to dive deeper and we want to give you the guidance/tools to do so. Read more below. ### How to run integration tests (Payload API tests) + There are a couple ways to do this: - **Granularly** - you can run individual tests in vscode by installing the Jest Runner plugin and using that to run individual tests. Clicking the `debug` button will run the test in debug mode allowing you to set break points. @@ -51,7 +54,9 @@ There are a couple ways to do this: ``` ### How to run E2E tests (Admin Panel UI tests) + The easiest way to run E2E tests is to install + - [Playwright Test for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) - [Playwright Runner](https://marketplace.visualstudio.com/items?itemName=ortoni.ortoni) @@ -59,6 +64,6 @@ Once they are installed you can open the `testing` tab in vscode sidebar and dri - #### Notes + - It is recommended to add the test credentials (located in `test/credentials.ts`) to your autofill for `localhost:3000/admin` as this will be required on every nodemon restart. The default credentials are `dev@payloadcms.com` as email and `test` as password. diff --git a/README.md b/README.md index 892120d67..5bcbf1f36 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ ## ☁️ Deploy instantly with Payload Cloud. + Create a cloud account, connect your GitHub, and [deploy in minutes](https://payloadcms.com/new). ## 🚀 Get started by self-hosting completely free, forever. @@ -52,7 +53,9 @@ npx create-payload-app Alternatively, it only takes about five minutes to [create an app from scratch](https://payloadcms.com/docs/getting-started/installation#from-scratch). ## 🖱️ One-click templates + ### 🛒 [E-Commerce](https://github.com/payloadcms/payload/tree/master/templates/ecommerce) + Eliminate the need to combine Shopify and a CMS, and instead do it all with Payload + Stripe. Best of all, you can extend it as much as you need. [All Official Templates](https://github.com/orgs/payloadcms/repositories?q=topic%3Apayload-template) · [Community Templates](https://github.com/topics/payload-template) diff --git a/docs/access-control/collections.mdx b/docs/access-control/collections.mdx index 9b0969de1..2fa681add 100644 --- a/docs/access-control/collections.mdx +++ b/docs/access-control/collections.mdx @@ -10,23 +10,24 @@ You can define Collection-level Access Control within each Collection's `access` ## Available Controls -| Function | Allows/Denies Access | -| ------------------------ | -------------------- | -| **[`create`](#create)** | Used in the `create` operation | -| **[`read`](#read)** | Used in the `find` and `findByID` operations | -| **[`update`](#update)** | Used in the `update` operation | -| **[`delete`](#delete)** | Used in the `delete` operation | +| Function | Allows/Denies Access | +| ----------------------- | -------------------------------------------- | +| **[`create`](#create)** | Used in the `create` operation | +| **[`read`](#read)** | Used in the `find` and `findByID` operations | +| **[`update`](#update)** | Used in the `update` operation | +| **[`delete`](#delete)** | Used in the `delete` operation | #### Auth-enabled Controls If a Collection supports [`Authentication`](/docs/authentication/overview), the following Access Controls become available: -| Function | Allows/Denies Access | -| ----------------------- | -------------------- | -| **[`admin`](#admin)** | Used to restrict access to the Payload Admin panel | +| Function | Allows/Denies Access | +| ----------------------- | -------------------------------------------------------------- | +| **[`admin`](#admin)** | Used to restrict access to the Payload Admin panel | | **[`unlock`](#unlock)** | Used to restrict which users can access the `unlock` operation | **Example Collection config:** + ```ts import { CollectionConfig } from 'payload/types'; @@ -50,10 +51,10 @@ Returns a boolean which allows/denies access to the `create` request. **Available argument properties:** -| Option | Description | -| ---------- | ----------- | +| Option | Description | +| ---------- | -------------------------------------------------------------------------- | | **`req`** | The Express `request` object containing the currently authenticated `user` | -| **`data`** | The data passed to create the document with. | +| **`data`** | The data passed to create the document with. | **Example:** @@ -77,20 +78,20 @@ Read access functions can return a boolean result or optionally return a [query **Available argument properties:** -| Option | Description | -| --------- | ----------- | +| Option | Description | +| --------- | -------------------------------------------------------------------------- | | **`req`** | The Express `request` object containing the currently authenticated `user` | -| **`id`** | `id` of document requested, if within `findByID` | +| **`id`** | `id` of document requested, if within `findByID` | **Example:** ```ts -import { Access } from 'payload/config'; +import { Access } from 'payload/config' const canReadPage: Access = ({ req: { user } }) => { // allow authenticated users if (user) { - return true; + return true } // using a query constraint, guest users can access when a field named 'isPublic' is set to true return { @@ -99,7 +100,7 @@ const canReadPage: Access = ({ req: { user } }) => { equals: true, }, } -}; +} ``` ### Update @@ -108,25 +109,25 @@ Update access functions can return a boolean result or optionally return a [quer **Available argument properties:** -| Option | Description | -| ---------- | ----------- | +| Option | Description | +| ---------- | -------------------------------------------------------------------------- | | **`req`** | The Express `request` object containing the currently authenticated `user` | -| **`id`** | `id` of document requested to update | -| **`data`** | The data passed to update the document with | +| **`id`** | `id` of document requested to update | +| **`data`** | The data passed to update the document with | **Example:** ```ts -import { Access } from 'payload/config'; +import { Access } from 'payload/config' const canUpdateUser: Access = ({ req: { user }, id }) => { // allow users with a role of 'admin' - if (user.roles && user.roles.some(role => role === 'admin')) { - return true; + if (user.roles && user.roles.some((role) => role === 'admin')) { + return true } // allow any other users to update only oneself - return user.id === id; -}; + return user.id === id +} ``` ### Delete @@ -135,10 +136,10 @@ Similarly to the Update function, returns a boolean or a [query constraint](/doc **Available argument properties:** -| Option | Description | -| --------- | ----------- | +| Option | Description | +| --------- | --------------------------------------------------------------------------------------------------- | | **`req`** | The Express `request` object with additional `user` property, which is the currently logged in user | -| **`id`** | `id` of document requested to delete | +| **`id`** | `id` of document requested to delete | **Example:** @@ -148,7 +149,7 @@ import { Access } from 'payload/config' const canDeleteCustomer: Access = async ({ req, id }) => { if (!id) { // allow the admin UI to show controls to delete since it is indeterminate without the id - return true; + return true } // query another collection using the id const result = await req.payload.find({ @@ -158,10 +159,10 @@ const canDeleteCustomer: Access = async ({ req, id }) => { where: { customer: { equals: id }, }, - }); + }) - return result.totalDocs === 0; -}; + return result.totalDocs === 0 +} ``` ### Admin @@ -170,8 +171,8 @@ If the Collection is [used to access the Payload Admin panel](/docs/admin/overvi **Available argument properties:** -| Option | Description | -| --------- | ----------- | +| Option | Description | +| --------- | -------------------------------------------------------------------------- | | **`req`** | The Express `request` object containing the currently authenticated `user` | ### Unlock @@ -180,6 +181,6 @@ Determines which users can [unlock](/docs/authentication/operations#unlock) othe **Available argument properties:** -| Option | Description | -| --------- | ----------- | +| Option | Description | +| --------- | -------------------------------------------------------------------------- | | **`req`** | The Express `request` object containing the currently authenticated `user` | diff --git a/docs/access-control/fields.mdx b/docs/access-control/fields.mdx index 513f140e9..68cc782ed 100644 --- a/docs/access-control/fields.mdx +++ b/docs/access-control/fields.mdx @@ -10,13 +10,14 @@ Field Access Control is specified with functions inside a field's config. All fi ## Available Controls -| Function | Purpose | -| ------------------------ | ------- | -| **[`create`](#create)** | Allows or denies the ability to set a field's value when creating a new document | -| **[`read`](#read)** | Allows or denies the ability to read a field's value | -| **[`update`](#update)** | Allows or denies the ability to update a field's value | +| Function | Purpose | +| ----------------------- | -------------------------------------------------------------------------------- | +| **[`create`](#create)** | Allows or denies the ability to set a field's value when creating a new document | +| **[`read`](#read)** | Allows or denies the ability to read a field's value | +| **[`update`](#update)** | Allows or denies the ability to update a field's value | **Example Collection config:** + ```ts import { CollectionConfig } from 'payload/types'; @@ -44,11 +45,11 @@ Returns a boolean which allows or denies the ability to set a field's value when **Available argument properties:** -| Option | Description | -| ----------------- | ----------- | +| Option | Description | +| ----------------- | -------------------------------------------------------------------------- | | **`req`** | The Express `request` object containing the currently authenticated `user` | -| **`data`** | The full data passed to create the document. | -| **`siblingData`** | Immediately adjacent field data passed to create the document. | +| **`data`** | The full data passed to create the document. | +| **`siblingData`** | Immediately adjacent field data passed to create the document. | ### Read @@ -56,12 +57,12 @@ Returns a boolean which allows or denies the ability to read a field's value. If **Available argument properties:** -| Option | Description | -| ----------------- | ----------- | +| Option | Description | +| ----------------- | -------------------------------------------------------------------------- | | **`req`** | The Express `request` object containing the currently authenticated `user` | -| **`id`** | `id` of the document being read | -| **`doc`** | The full document data. | -| **`siblingData`** | Immediately adjacent field data of the document being read. | +| **`id`** | `id` of the document being read | +| **`doc`** | The full document data. | +| **`siblingData`** | Immediately adjacent field data of the document being read. | ### Update @@ -71,10 +72,10 @@ If `false` is returned and you attempt to update the field's value, the operatio **Available argument properties:** -| Option | Description | -| ----------------- | ----------- | +| Option | Description | +| ----------------- | -------------------------------------------------------------------------- | | **`req`** | The Express `request` object containing the currently authenticated `user` | -| **`id`** | `id` of the document being updated | -| **`data`** | The full data passed to update the document. | -| **`siblingData`** | Immediately adjacent field data passed to update the document with. | -| **`doc`** | The full document data, before the update is applied. | +| **`id`** | `id` of the document being updated | +| **`data`** | The full data passed to update the document. | +| **`siblingData`** | Immediately adjacent field data passed to update the document with. | +| **`doc`** | The full document data, before the update is applied. | diff --git a/docs/access-control/globals.mdx b/docs/access-control/globals.mdx index bdb73b9fa..cd8f8673e 100644 --- a/docs/access-control/globals.mdx +++ b/docs/access-control/globals.mdx @@ -8,30 +8,35 @@ keywords: globals, access control, permissions, documentation, Content Managemen You can define Global-level Access Control within each Global's `access` property. All Access Control functions accept one `args` argument. -**Available argument properties: +\*\*Available argument properties: ## Available Controls -| Function | Allows/Denies Access | -| ------------------------ | -------------------- | -| **[`read`](#read)** | Used in the `findOne` Global operation | -| **[`update`](#update)** | Used in the `update` Global operation | +| Function | Allows/Denies Access | +| ----------------------- | -------------------------------------- | +| **[`read`](#read)** | Used in the `findOne` Global operation | +| **[`update`](#update)** | Used in the `update` Global operation | **Example Global config:** + ```ts -import { GlobalConfig } from 'payload/types'; +import { GlobalConfig } from 'payload/types' const Header: GlobalConfig = { - slug: "header", + slug: 'header', // highlight-start access: { - read: ({ req: { user } }) => { /* */ }, - update: ({ req: { user } }) => { /* */ }, + read: ({ req: { user } }) => { + /* */ + }, + update: ({ req: { user } }) => { + /* */ + }, }, // highlight-end -}; +} -export default Header; +export default Header ``` ### Read @@ -40,8 +45,8 @@ Returns a boolean result or optionally a [query constraint](/docs/queries/overvi **Available argument properties:** -| Option | Description | -| --------- | ----------- | +| Option | Description | +| --------- | -------------------------------------------------------------------------- | | **`req`** | The Express `request` object containing the currently authenticated `user` | ### Update @@ -50,7 +55,7 @@ Returns a boolean result or optionally a [query constraint](/docs/queries/overvi **Available argument properties:** -| Option | Description | -| ---------- | ----------- | +| Option | Description | +| ---------- | -------------------------------------------------------------------------- | | **`req`** | The Express `request` object containing the currently authenticated `user` | -| **`data`** | The data passed to update the global with. | +| **`data`** | The data passed to update the global with. | diff --git a/docs/access-control/overview.mdx b/docs/access-control/overview.mdx index 7d5a51106..03d8596a2 100644 --- a/docs/access-control/overview.mdx +++ b/docs/access-control/overview.mdx @@ -8,10 +8,7 @@ keywords: overview, access control, permissions, documentation, Content Manageme Access control within Payload is extremely powerful while remaining easy and intuitive to manage. Declaring who should have access to what documents is no more complex than writing a simple JavaScript function that either returns a `boolean` or a [`query`](/docs/queries/overview) constraint to restrict which documents users can interact with. - + **Example use cases:** @@ -32,13 +29,18 @@ Access control within Payload is extremely powerful while remaining easy and int const defaultPayloadAccess = ({ req: { user } }) => { // Return `true` if a user is found // and `false` if it is undefined or null - return Boolean(user); + return Boolean(user) } ``` - Note:
- In the Local API, all Access Control functions are skipped by default, allowing your server to do whatever it needs. But, you can opt back in by setting the option overrideAccess to false. + Note: +
+ In the Local API, all Access Control functions are skipped by default, allowing your server to do + whatever it needs. But, you can opt back in by setting the option + overrideAccess + {' '} + to false.
### Access Control Types @@ -49,12 +51,13 @@ You can manage access within Payload on three different levels: - [Fields](/docs/access-control/fields) - [Globals](/docs/access-control/globals) - ### When Access Control is Executed - Note:
- Access control functions are utilized in two places. It's important to understand how and when your access control is executed. + Note: +
+ Access control functions are utilized in two places. It's important to understand how and when + your access control is executed.
#### As you execute operations @@ -70,8 +73,11 @@ To accomplish this, Payload ships with an `Access` operation, which is executed ### Argument Availability - Important:
- When your access control functions are executed via the access operation, the id and data arguments will be undefined, because Payload is executing your functions without referencing a specific document. + Important: +
+ When your access control functions are executed via the access operation, the{' '} + id and data arguments will be undefined, + because Payload is executing your functions without referencing a specific document.
If you use `id` or `data` within your access control functions, make sure to check that they are defined first. If they are not, then you can assume that your access control is being executed via the `access` operation, to determine solely what the user can do within the Admin UI. diff --git a/docs/admin/components.mdx b/docs/admin/components.mdx index 60c37110f..22f220e33 100644 --- a/docs/admin/components.mdx +++ b/docs/admin/components.mdx @@ -11,8 +11,10 @@ While designing the Payload Admin panel, we determined it should be as minimal a To swap in your own React component, first, consult the list of available component overrides below. Determine the scope that corresponds to what you are trying to accomplish, and then author your React component accordingly. - Tip:
- Custom components will automatically be provided with all props that the default component would accept. + Tip: +
+ Custom components will automatically be provided with all props that the default component would + accept.
### Base Component Overrides @@ -41,7 +43,7 @@ You can override a set of admin panel-wide components by providing a component t `payload.config.js` ```ts -import { buildConfig } from "payload/config"; +import { buildConfig } from 'payload/config' import { MyCustomNav, MyCustomLogo, @@ -49,7 +51,7 @@ import { MyCustomAccount, MyCustomDashboard, MyProvider, -} from "./customComponents"; +} from './customComponents' export default buildConfig({ admin: { @@ -66,7 +68,7 @@ export default buildConfig({ providers: [MyProvider], }, }, -}); +}) ``` _For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/master/test/admin/components)._ @@ -93,20 +95,17 @@ You can override components on a Collection-by-Collection basis via each Collect ```tsx // Custom Buttons -import * as React from "react"; +import * as React from 'react' import { CustomSaveButtonProps, CustomSaveDraftButtonProps, CustomPublishButtonProps, CustomPreviewButtonProps, -} from "payload/types"; +} from 'payload/types' -export const CustomSaveButton: CustomSaveButtonProps = ({ - DefaultButton, - label, -}) => { - return ; -}; +export const CustomSaveButton: CustomSaveButtonProps = ({ DefaultButton, label }) => { + return +} export const CustomSaveDraftButton: CustomSaveDraftButtonProps = ({ DefaultButton, @@ -114,10 +113,8 @@ export const CustomSaveDraftButton: CustomSaveDraftButtonProps = ({ label, saveDraft, }) => { - return ( - - ); -}; + return +} export const CustomPublishButton: CustomPublishButtonProps = ({ DefaultButton, @@ -125,8 +122,8 @@ export const CustomPublishButton: CustomPublishButtonProps = ({ label, publish, }) => { - return ; -}; + return +} export const CustomPreviewButton: CustomPreviewButtonProps = ({ DefaultButton, @@ -134,8 +131,8 @@ export const CustomPreviewButton: CustomPreviewButtonProps = ({ label, preview, }) => { - return ; -}; + return +} ``` ##### Custom Collection List View Example @@ -162,17 +159,17 @@ export const MyCollection: CollectionConfig = { MyListComponent.tsx ```tsx -import React from "react"; -import { List, type Props } from "payload/components/views/List"; // Payload's default List view component and its props +import React from 'react' +import { List, type Props } from 'payload/components/views/List' // Payload's default List view component and its props export const MyListComponent: React.FC = (props) => (

- Some text before the default list view component. If you just want to do - that, you can also use the admin.components.list.BeforeList hook + Some text before the default list view component. If you just want to do that, you can also + use the admin.components.list.BeforeList hook

-); +) ``` ### Globals @@ -194,10 +191,9 @@ All Payload fields support the ability to swap in your own React components. So, Tip:
- Don't see a built-in field type that you need? Build it! Using a combination - of custom validation and custom components, you can override the entirety of - how a component functions within the admin panel and effectively create your - own field type. + Don't see a built-in field type that you need? Build it! Using a combination of custom validation + and custom components, you can override the entirety of how a component functions within the admin + panel and effectively create your own field type.
**Fields support the following custom components:** @@ -223,15 +219,15 @@ These are the props that will be passed to your custom Cell to use in your own c #### Example ```tsx -import React from "react"; -import "./index.scss"; -const baseClass = "custom-cell"; +import React from 'react' +import './index.scss' +const baseClass = 'custom-cell' const CustomCell: React.FC = (props) => { - const { field, colIndex, collection, cellData, rowData } = props; + const { field, colIndex, collection, cellData, rowData } = props - return {cellData}; -}; + return {cellData} +} ``` ## Field Component @@ -243,25 +239,22 @@ When writing your own custom components you can make use of a number of hooks to When swapping out the `Field` component, you'll be responsible for sending and receiving the field's `value` from the form itself. To do so, import the `useField` hook as follows: ```tsx -import { useField } from "payload/components/forms"; +import { useField } from 'payload/components/forms' -type Props = { path: string }; +type Props = { path: string } const CustomTextField: React.FC = ({ path }) => { // highlight-start - const { value, setValue } = useField({ path }); + const { value, setValue } = useField({ path }) // highlight-end - return ( - setValue(e.target.value)} value={value.path} /> - ); -}; + return setValue(e.target.value)} value={value.path} /> +} ``` - For more information regarding the hooks that are available to you while you - build custom components, including the useField hook, [click - here](/docs/admin/hooks). + For more information regarding the hooks that are available to you while you build custom + components, including the useField hook, [click here](/docs/admin/hooks). ## Custom routes @@ -292,9 +285,8 @@ Your custom route components will be given all the props that a React Router ` Note:
- It's up to you to secure your custom routes. If your route requires a user to - be logged in or to have certain access rights, you should handle that within - your route component yourself. + It's up to you to secure your custom routes. If your route requires a user to be logged in or to + have certain access rights, you should handle that within your route component yourself. #### Example @@ -311,8 +303,8 @@ To see how to pass in your custom views to create custom routes of your own, tak As your admin customizations gets more complex you may want to share state between fields or other components. You can add custom providers to do add your own context to any Payload app for use in other custom components within the admin panel. Within your config add `admin.components.providers`, these can be used to share context or provide other custom functionality. Read the [React context](https://reactjs.org/docs/context.html) docs to learn more. - Reminder: Don't forget to pass the **children** prop through - the provider component for the admin UI to show + Reminder: Don't forget to pass the **children** prop through the provider + component for the admin UI to show ### Styling Custom Components @@ -332,21 +324,21 @@ When developing custom components you can support multiple languages to be consi For example: ```tsx -import { useTranslation } from "react-i18next"; +import { useTranslation } from 'react-i18next' const CustomComponent: React.FC = () => { // highlight-start - const { t, i18n } = useTranslation("namespace1"); + const { t, i18n } = useTranslation('namespace1') // highlight-end return (
    -
  • {t("key", { variable: "value" })}
  • -
  • {t("namespace2:key", { variable: "value" })}
  • +
  • {t('key', { variable: 'value' })}
  • +
  • {t('namespace2:key', { variable: 'value' })}
  • {i18n.language}
- ); -}; + ) +} ``` ### Getting the current locale @@ -354,18 +346,18 @@ const CustomComponent: React.FC = () => { In any custom component you can get the selected locale with `useLocale` hook. `useLocale` returns the full locale object, consisting of a `label`, `rtl`(right-to-left) property, and then `code`. Here is a simple example: ```tsx -import { useLocale } from "payload/components/utilities"; +import { useLocale } from 'payload/components/utilities' const Greeting: React.FC = () => { // highlight-start - const locale = useLocale(); + const locale = useLocale() // highlight-end const trans = { - en: "Hello", - es: "Hola", - }; + en: 'Hello', + es: 'Hola', + } return {trans[locale.code]} -}; +} ``` diff --git a/docs/admin/customizing-css.mdx b/docs/admin/customizing-css.mdx index 85c79c8f5..1592418b5 100644 --- a/docs/admin/customizing-css.mdx +++ b/docs/admin/customizing-css.mdx @@ -13,15 +13,16 @@ You can add your own CSS by providing your base Payload config with a path to yo To do so, provide your base Payload config with a path to your own stylesheet. It can be either a CSS or SCSS file. **Example in payload.config.js:** + ```ts -import { buildConfig } from 'payload/config'; -import path from 'path'; +import { buildConfig } from 'payload/config' +import path from 'path' const config = buildConfig({ - admin: { - css: path.resolve(__dirname, 'relative/path/to/stylesheet.scss'), - }, -}); + admin: { + css: path.resolve(__dirname, 'relative/path/to/stylesheet.scss'), + }, +}) ``` ### Overriding built-in styles @@ -43,7 +44,8 @@ You can find the built-in Payload CSS variables within [`./src/admin/scss/app.sc #### Dark mode - If you're overriding colors or theme elevations, make sure to consider how your changes will affect dark mode. + If you're overriding colors or theme elevations, make sure to consider how your changes will + affect dark mode. By default, Payload automatically overrides all `--theme-elevation`s and inverts all success / warning / error shades to suit dark mode. We also update some base theme variables like `--theme-bg`, `--theme-text`, etc. diff --git a/docs/admin/hooks.mdx b/docs/admin/hooks.mdx index 3c498db6a..ea4b21903 100644 --- a/docs/admin/hooks.mdx +++ b/docs/admin/hooks.mdx @@ -24,7 +24,7 @@ const CustomTextField: React.FC = ({ path }) => { const { value, setValue } = useField({ path }) // highlight-end - return setValue(e.target.value)} value={value.path} /> + return setValue(e.target.value)} value={value.path} /> } ``` @@ -57,7 +57,8 @@ const { There are times when a custom field component needs to have access to data from other fields, and you have a few options to do so. The `useFormFields` hook is a powerful and highly performant way to retrieve a form's field state, as well as to retrieve the `dispatchFields` method, which can be helpful for setting other fields' form states from anywhere within a form. - This hook is great for retrieving only certain fields from form state because it ensures that it will only cause a rerender when the items that you ask for change. + This hook is great for retrieving only certain fields from form state because it + ensures that it will only cause a rerender when the items that you ask for change. Thanks to the awesome package [`use-context-selector`](https://github.com/dai-shi/use-context-selector), you can retrieve a specific field's state easily. This is ideal because you can ensure you have an up-to-date field state, and your component will only re-render when _that field's state_ changes. @@ -65,21 +66,19 @@ Thanks to the awesome package [`use-context-selector`](https://github.com/dai-sh You can pass a Redux-like selector into the hook, which will ensure that you retrieve only the field that you want. The selector takes an argument with type of `[fields: Fields, dispatch: React.Dispatch]]`. ```tsx -import { useFormFields } from 'payload/components/forms'; +import { useFormFields } from 'payload/components/forms' const MyComponent: React.FC = () => { // Get only the `amount` field state, and only cause a rerender when that field changes - const amount = useFormFields(([fields, dispatch]) => fields.amount); + const amount = useFormFields(([fields, dispatch]) => fields.amount) // Do the same thing as above, but to the `feePercentage` field - const feePercentage = useFormFields(([fields, dispatch]) => fields.feePercentage); + const feePercentage = useFormFields(([fields, dispatch]) => fields.feePercentage) if (typeof amount?.value !== 'undefined' && typeof feePercentage?.value !== 'undefined') { - return ( - The fee is ${(amount.value * feePercentage.value) / 100} - ); + return The fee is ${(amount.value * feePercentage.value) / 100} } -}; +} ``` ### useAllFormFields @@ -117,7 +116,7 @@ If you are building a custom component, then you should use `setValue` which is You can send the following actions to the `dispatchFields` function. | Action | Description | -|------------------------|----------------------------------------------------------------------------| +| ---------------------- | -------------------------------------------------------------------------- | | **`ADD_ROW`** | Adds a row of data (useful in array / block field data) | | **`DUPLICATE_ROW`** | Duplicates a row of data (useful in array / block field data) | | **`MODIFY_CONDITION`** | Updates a field's conditional logic result (true / false) | @@ -134,8 +133,12 @@ To see types for each action supported within the `dispatchFields` hook, check o The `useForm` hook can be used to interact with the form itself, and sends back many methods that can be used to reactively fetch form state without causing rerenders within your components each time a field is changed. This is useful if you have action-based callbacks that your components fire, and need to interact with form state _based on a user action_. - Warning:
- This hook is optimized to avoid causing rerenders when fields change, and as such, its `fields` property will be out of date. You should only leverage this hook if you need to perform actions against the form in response to your users' actions. Do not rely on its returned "fields" as being up-to-date. They will be removed from this hook's response in an upcoming version. + Warning: +
+ This hook is optimized to avoid causing rerenders when fields change, and as such, its `fields` + property will be out of date. You should only leverage this hook if you need to perform actions + against the form in response to your users' actions. Do not rely on its returned "fields" as being + up-to-date. They will be removed from this hook's response in an upcoming version.
The `useForm` hook returns an object with the following properties: | @@ -358,10 +361,14 @@ The `useForm` hook returns an object with the following properties: | ]} /> -
+{' '} -
-{`import { useForm } from "payload/components/forms";
+
+ +{' '} + +
+  {`import { useForm } from "payload/components/forms";
 
 export const CustomArrayManager = () => {
   const { addFieldRow } = useForm()
@@ -385,7 +392,7 @@ export const CustomArrayManager = () => {
     
   )
 }`}
-  
+

An example config to go along with the custom component

@@ -456,10 +463,14 @@ export const CustomArrayManager = () => {
     ]}
   />
 
-  
+{' '} -
-{`import { useForm } from "payload/components/forms";
+
+ +{' '} + +
+  {`import { useForm } from "payload/components/forms";
 
 export const CustomArrayManager = () => {
   const { removeFieldRow } = useForm()
@@ -478,7 +489,7 @@ export const CustomArrayManager = () => {
     
   )
 }`}
-  
+

An example config to go along with the custom component

@@ -557,10 +568,14 @@ export const CustomArrayManager = () => {
     ]}
   />
 
-  
+{' '} -
-{`import { useForm } from "payload/components/forms";
+
+ +{' '} + +
+  {`import { useForm } from "payload/components/forms";
 
 export const CustomArrayManager = () => {
   const { replaceFieldRow } = useForm()
@@ -584,7 +599,7 @@ export const CustomArrayManager = () => {
     
   )
 }`}
-  
+

An example config to go along with the custom component

@@ -624,40 +639,40 @@ export const CustomArrayManager = () => {
 
 The `useDocumentInfo` hook provides lots of information about the document currently being edited, including the following:
 
-| Property                  | Description                                                                                                        |
-|---------------------------|--------------------------------------------------------------------------------------------------------------------|                                |
-| **`collection`**          | If the doc is a collection, its collection config will be returned                                                 |
-| **`global`**              | If the doc is a global, its global config will be returned                                                         |
-| **`id`**                  | If the doc is a collection, its ID will be returned                                                                |
-| **`preferencesKey`**      | The `preferences` key to use when interacting with document-level user preferences                                 |
-| **`versions`**            | Versions of the current doc                                                                                        |
-| **`unpublishedVersions`** | Unpublished versions of the current doc                                                                            |
-| **`publishedDoc`**        | The currently published version of the doc being edited                                                            |
-| **`getVersions`**         | Method to trigger the retrieval of document versions                                                               |
-| **`docPermissions`**      | The current documents permissions. Collection document permissions fallback when no id is present (i.e. on create) |
-| **`getDocPermissions`**   | Method to trigger the retrieval of document level permissions                                                      |
+| Property | Description |
+|---------------------------|--------------------------------------------------------------------------------------------------------------------| |
+| **`collection`** | If the doc is a collection, its collection config will be returned |
+| **`global`** | If the doc is a global, its global config will be returned |
+| **`id`** | If the doc is a collection, its ID will be returned |
+| **`preferencesKey`** | The `preferences` key to use when interacting with document-level user preferences |
+| **`versions`** | Versions of the current doc |
+| **`unpublishedVersions`** | Unpublished versions of the current doc |
+| **`publishedDoc`** | The currently published version of the doc being edited |
+| **`getVersions`** | Method to trigger the retrieval of document versions |
+| **`docPermissions`** | The current documents permissions. Collection document permissions fallback when no id is present (i.e. on create) |
+| **`getDocPermissions`** | Method to trigger the retrieval of document level permissions |
 
 **Example:**
 
 ```tsx
-import { useDocumentInfo } from 'payload/components/utilities';
+import { useDocumentInfo } from 'payload/components/utilities'
 
 const LinkFromCategoryToPosts: React.FC = () => {
   // highlight-start
-  const { id } = useDocumentInfo();
+  const { id } = useDocumentInfo()
   // highlight-end
 
   // id will be undefined on the create form
   if (!id) {
-    return null;
+    return null
   }
 
   return (
-    
+    
       View posts
     
   )
-};
+}
 ```
 
 ### useLocale
@@ -665,22 +680,20 @@ const LinkFromCategoryToPosts: React.FC = () => {
 In any custom component you can get the selected locale object with the `useLocale` hook. `useLocale`gives you the full locale object, consisting of a `label`, `rtl`(right-to-left) property, and then `code`. Here is a simple example:
 
 ```tsx
-import { useLocale } from 'payload/components/utilities';
+import { useLocale } from 'payload/components/utilities'
 
 const Greeting: React.FC = () => {
   // highlight-start
-  const locale = useLocale();
+  const locale = useLocale()
   // highlight-end
 
   const trans = {
     en: 'Hello',
     es: 'Hola',
-  };
+  }
 
-  return (
-     { trans[locale.code] } 
-  );
-};
+  return  {trans[locale.code]} 
+}
 ```
 
 ### useAuth
@@ -688,7 +701,7 @@ const Greeting: React.FC = () => {
 Useful to retrieve info about the currently logged in user as well as methods for interacting with it. It sends back an object with the following properties:
 
 | Property                 | Description                                                                             |
-|--------------------------|-----------------------------------------------------------------------------------------|
+| ------------------------ | --------------------------------------------------------------------------------------- |
 | **`user`**               | The currently logged in user                                                            |
 | **`logOut`**             | A method to log out the currently logged in user                                        |
 | **`refreshCookie`**      | A method to trigger the silent refreshing of a user's auth token                        |
@@ -698,18 +711,16 @@ Useful to retrieve info about the currently logged in user as well as methods fo
 | **`permissions`**        | The permissions of the current user                                                     |
 
 ```tsx
-import { useAuth } from 'payload/components/utilities';
-import { User } from '../payload-types.ts';
+import { useAuth } from 'payload/components/utilities'
+import { User } from '../payload-types.ts'
 
 const Greeting: React.FC = () => {
   // highlight-start
-  const { user } = useAuth();
+  const { user } = useAuth()
   // highlight-end
 
-  return (
-    Hi, {user.email}!
-  );
-};
+  return Hi, {user.email}!
+}
 ```
 
 ### useConfig
@@ -717,17 +728,15 @@ const Greeting: React.FC = () => {
 Used to easily fetch the full Payload config.
 
 ```tsx
-import { useConfig } from 'payload/components/utilities';
+import { useConfig } from 'payload/components/utilities'
 
 const MyComponent: React.FC = () => {
   // highlight-start
-  const config = useConfig();
+  const config = useConfig()
   // highlight-end
 
-  return (
-    {config.serverURL}
-  );
-};
+  return {config.serverURL}
+}
 ```
 
 ### useEditDepth
@@ -735,16 +744,14 @@ const MyComponent: React.FC = () => {
 Sends back how many editing levels "deep" the current component is. Edit depth is relevant while adding new documents / editing documents in modal windows and other cases.
 
 ```tsx
-import { useEditDepth } from 'payload/components/utilities';
+import { useEditDepth } from 'payload/components/utilities'
 
 const MyComponent: React.FC = () => {
   // highlight-start
-  const editDepth = useEditDepth();
+  const editDepth = useEditDepth()
   // highlight-end
 
-  return (
-    My component is {editDepth} levels deep
-  )
+  return My component is {editDepth} levels deep
 }
 ```
 
diff --git a/docs/admin/overview.mdx b/docs/admin/overview.mdx
index c042974b0..420d9ee74 100644
--- a/docs/admin/overview.mdx
+++ b/docs/admin/overview.mdx
@@ -11,9 +11,9 @@ Payload dynamically generates a beautiful, fully functional React admin panel to
 The Payload Admin panel is built with Webpack, code-split, highly performant (even with 100+ fields), and written fully in TypeScript.
 
 
-  The Admin panel is meant to be simple enough to give you a starting point but
-  not bring too much complexity, so that you can easily customize it to suit the
-  needs of your application and your editors.
+  The Admin panel is meant to be simple enough to give you a starting point but not bring too much
+  complexity, so that you can easily customize it to suit the needs of your application and your
+  editors.
 
 
 ![Payload's Admin panel built in React](https://payloadcms.com/images/docs/admin.jpg)
@@ -25,7 +25,7 @@ _Screenshot of the Admin panel while editing a document from an example `AllFiel
 All options for the Admin panel are defined in your base Payload config file.
 
 | Option                | Description                                                                                                                                                                                                                                                  |
-|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
 | `user`                | The `slug` of a Collection that you want be used to log in to the Admin dashboard. [More](/docs/admin/overview#the-admin-user-collection)                                                                                                                    |
 | `buildPath`           | Specify an absolute path for where to store the built Admin panel bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`.                                                                                                             |
 | `meta`                | Base meta data to use for the Admin panel. Included properties are `titleSuffix`, `ogImage`, and `favicon`.                                                                                                                                                  |
@@ -55,13 +55,13 @@ To specify which Collection to use to log in to the Admin panel, pass the `admin
 `payload.config.js`:
 
 ```ts
-import { buildConfig } from "payload/config";
+import { buildConfig } from 'payload/config'
 
 const config = buildConfig({
   admin: {
-    user: "admins", // highlight-line
+    user: 'admins', // highlight-line
   },
-});
+})
 ```
 
 By default, if you have not specified a Collection, Payload will automatically provide you with a `User` Collection which will be used to access the Admin panel. You can customize or override the fields and settings of the default `User` Collection by passing your own collection using `users` as its `slug` to Payload. When this is done, Payload will use your provided `User` Collection instead of its default version.
diff --git a/docs/admin/preferences.mdx b/docs/admin/preferences.mdx
index 4343acde6..b4f8ce491 100644
--- a/docs/admin/preferences.mdx
+++ b/docs/admin/preferences.mdx
@@ -15,8 +15,10 @@ Out of the box, Payload handles the persistence of your users' preferences in a
 1. The "collapsed" state of blocks, on a document level, as users edit or interact with documents
 
 
-	Important:
- All preferences are stored on an individual user basis. Payload automatically recognizes the user that is reading or setting a preference via all provided authentication methods. + Important: +
+ All preferences are stored on an individual user basis. Payload automatically recognizes the user + that is reading or setting a preference via all provided authentication methods.
### Use cases @@ -33,7 +35,7 @@ This API is used significantly for internal operations of the Admin panel, as me Payload automatically creates an internally used `payload-preferences` collection that stores user preferences. Each document in the `payload-preferences` collection contains the following shape: | Key | Value | -|-------------------|-------------------------------------------------------------------| +| ----------------- | ----------------------------------------------------------------- | | `id` | A unique ID for each preference stored. | | `key` | A unique `key` that corresponds to the preference. | | `user.value` | The ID of the `user` that is storing its preference. | diff --git a/docs/admin/webpack.mdx b/docs/admin/webpack.mdx index ddfa1599f..5e87c4f28 100644 --- a/docs/admin/webpack.mdx +++ b/docs/admin/webpack.mdx @@ -11,20 +11,21 @@ Payload uses Webpack 5 to build the Admin panel. It comes with support for many To extend the Webpack config, add the `webpack` key to your base Payload config, and provide a function that accepts the default Webpack config as its only argument: `payload.config.ts` + ```ts -import { buildConfig } from 'payload/config'; +import { buildConfig } from 'payload/config' export default buildConfig({ - admin: { - // highlight-start - webpack: (config) => { - // Do something with the config here + admin: { + // highlight-start + webpack: (config) => { + // Do something with the config here - return config; - } - // highlight-end - } -}); + return config + }, + // highlight-end + }, +}) ``` ### Aliasing server-only modules @@ -43,73 +44,84 @@ Examples of **non** browser-friendly packages: You may rely on server-only packages such as the above to perform logic in access control functions, hooks, and other contexts (which is great!) but when you boot up your Payload app and try to view it in the browser, you'll likely run into missing dependency issues or other general incompatibilities. - Tip:
- To avoid problems with server code making it to your Webpack bundle, you can use the alias Webpack feature to tell Webpack to avoid importing the modules you want to restrict to server-only. + Tip: +
+ To avoid problems with server code making it to your Webpack bundle, you can use the{' '} + alias Webpack feature to tell Webpack to avoid importing the modules you want to + restrict to server-only.
-For example, let's say that you have a Collection called `Subscriptions` which relies on Stripe: + + For example, let's say that you have a Collection called `Subscriptions` which relies on Stripe: + -

+
+
`collections/Subscriptions/index.js` + ```ts -import { CollectionConfig } from 'payload/types'; -import createStripeSubscription from './hooks/createStripeSubscription'; +import { CollectionConfig } from 'payload/types' +import createStripeSubscription from './hooks/createStripeSubscription' export const Subscription: CollectionConfig = { - slug: 'subscriptions', - hooks: { - beforeChange: [ - createStripeSubscription, - ] - }, - fields: [ - { - name: 'stripeSubscriptionID', - type: 'text', - required: true, - } - ] -}; + slug: 'subscriptions', + hooks: { + beforeChange: [createStripeSubscription], + }, + fields: [ + { + name: 'stripeSubscriptionID', + type: 'text', + required: true, + }, + ], +} ``` The collection above features a `beforeChange` hook that creates a Stripe subscription whenever a Subscription document is created in Payload. That hook might look something like this: -

+
+
`collections/Subscriptions/hooks/createStripeSubscription.js` -```js -import Stripe from 'stripe'; -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); +```js +import Stripe from 'stripe' + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY) const createStripeSubscription = async ({ data, operation }) => { - if (operation === 'create') { - const dataWithStripeID = {...data}; + if (operation === 'create') { + const dataWithStripeID = { ...data } - // use Stripe to create a Stripe subscription - const subscription = await stripe.subscriptions.create({ - // Configure the subscription accordingly - }); + // use Stripe to create a Stripe subscription + const subscription = await stripe.subscriptions.create({ + // Configure the subscription accordingly + }) - // Automatically add the Stripe subscription ID - // to the data that will be saved to this Subscription doc - dataWithStripeID.stripeSubscriptionID = subscription.id; + // Automatically add the Stripe subscription ID + // to the data that will be saved to this Subscription doc + dataWithStripeID.stripeSubscriptionID = subscription.id return dataWithStripeID - } + } - return data; + return data } -export default createStripeSubscription; +export default createStripeSubscription ``` - Warning:
- The above code is NOT production-ready and should not be referenced to create Stripe subscriptions. Although creating a beforeChange hook is a completely valid spot to do things like create subscriptions, the code above is incomplete and insecure, meant for explanation purposes only. + Warning: +
+ The above code is NOT production-ready and should not be referenced to create Stripe + subscriptions. Although creating a beforeChange hook is a completely valid spot to do things like + create subscriptions, the code above is incomplete and insecure, meant for explanation purposes + only.
**As-is, this collection will prevent your Admin panel from bundling or loading correctly, because Stripe relies on some Node-only packages.** @@ -117,52 +129,61 @@ export default createStripeSubscription; To remedy this issue you can extend the Payload Webpack config to alias your entire `createStripeSubscription` hook to a separate, empty mock file. Example in `payload.config.js`: -```js -import { buildConfig } from 'payload/config'; -import path from 'path'; -import Subscription from './collections/Subscription'; -const createStripeSubscriptionPath = path.resolve(__dirname, 'collections/Subscription/hooks/createStripeSubscription.js'); -const mockModulePath = path.resolve(__dirname, 'mocks/emptyObject.js'); +```js +import { buildConfig } from 'payload/config' +import path from 'path' +import Subscription from './collections/Subscription' + +const createStripeSubscriptionPath = path.resolve( + __dirname, + 'collections/Subscription/hooks/createStripeSubscription.js', +) +const mockModulePath = path.resolve(__dirname, 'mocks/emptyObject.js') export default buildConfig({ - collections: [ - Subscription - ], - admin: { - webpack: (config) => ({ - ...config, - resolve: { - ...config.resolve, - alias: { - ...config.resolve.alias, - [createStripeSubscriptionPath]: mockModulePath, - } - } - }) - } -}); + collections: [Subscription], + admin: { + webpack: (config) => ({ + ...config, + resolve: { + ...config.resolve, + alias: { + ...config.resolve.alias, + [createStripeSubscriptionPath]: mockModulePath, + }, + }, + }), + }, +}) ``` The above code will alias the file at path `createStripeSubscriptionPath` to a mocked module, which might look like this: `mocks/emptyObject.js` + ```js -export default {}; +export default {} ``` Now, when Webpack sees that you're attempting to import your `createStripeSubscriptionPath` file, it'll disregard that actual file and load your mock file instead. Not only will your Admin panel now bundle successfully, you will have optimized its filesize by removing unnecessary code! And you might have learned something about Webpack, too. - Tip:
- If changes to your Webpack aliases are not surfacing, they might be [cached](https://webpack.js.org/configuration/cache/) in `node_modules/.cache/webpack`. Try deleting that folder and restarting your server. + Tip: +
+ If changes to your Webpack aliases are not surfacing, they might be + [cached](https://webpack.js.org/configuration/cache/) in `node_modules/.cache/webpack`. Try + deleting that folder and restarting your server.
## Admin environment vars - Important:
- Be careful about what variables you provide to your client-side code. Analyze every single one to make sure that you're not accidentally leaking anything that an attacker could exploit. Only keys that are safe to be available to everyone in plain text should be provided to your Admin panel. + Important: +
+ Be careful about what variables you provide to your client-side code. Analyze every single one to + make sure that you're not accidentally leaking anything that an attacker could exploit. Only keys + that are safe to be available to everyone in plain text should be provided to your Admin panel.
By default, `env` variables are **not** provided to the Admin panel for security and safety reasons. But, Payload provides you with a way to still provide `env` vars to your frontend code. diff --git a/docs/authentication/config.mdx b/docs/authentication/config.mdx index 92b6d0a31..5e1f92bc4 100644 --- a/docs/authentication/config.mdx +++ b/docs/authentication/config.mdx @@ -41,8 +41,8 @@ Technically, both of these options will work for third-party integrations but th To enable API keys on a collection, set the `useAPIKey` auth option to `true`. From there, a new interface will appear in the Admin panel for each document within the collection that allows you to generate an API key for each user in the Collection. - User API keys are encrypted within the database, meaning that if your database - is compromised, your API keys will not be. + User API keys are encrypted within the database, meaning that if your database is compromised, + your API keys will not be. #### Authenticating via API Key @@ -52,31 +52,31 @@ To authenticate REST or GraphQL API requests using an API key, set the `Authoriz **For example, using Fetch:** ```ts -import User from '../collections/User'; +import User from '../collections/User' -const response = await fetch("http://localhost:3000/api/pages", { +const response = await fetch('http://localhost:3000/api/pages', { headers: { Authorization: `${User.slug} API-Key ${YOUR_API_KEY}`, }, -}); +}) ``` Payload ensures that the same, uniform access control is used across all authentication strategies. This enables you to utilize your existing access control configurations with both API keys and the standard email/password authentication. This consistency can aid in maintaining granular control over your API keys. -#### API Key *Only* Authentication +#### API Key _Only_ Authentication If you want to use API keys as the only authentication method for a collection, you can disable the default local strategy by setting `disableLocalStrategy` to `true` on the collection's `auth` property. This will disable the ability to authenticate with email and password, and will only allow for authentication via API key. ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const Customers: CollectionConfig = { slug: 'customers', auth: { useAPIKey: true, disableLocalStrategy: true, - } -}; + }, +} ``` ### Forgot Password @@ -90,17 +90,16 @@ Function that accepts one argument, containing `{ req, token, user }`, that allo Tip:
- HTML templating can be used to create custom email templates, inline CSS - automatically, and more. You can make a reusable function that standardizes - all email sent from Payload, which makes sending custom emails more DRY. - Payload doesn't ship with an HTML templating engine, so you are free to choose - your own. + HTML templating can be used to create custom email templates, inline CSS automatically, and more. + You can make a reusable function that standardizes all email sent from Payload, which makes + sending custom emails more DRY. Payload doesn't ship with an HTML templating engine, so you are + free to choose your own.
Example: ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const Customers: CollectionConfig = { slug: 'customers', @@ -109,7 +108,7 @@ export const Customers: CollectionConfig = { // highlight-start generateEmailHTML: ({ req, token, user }) => { // Use the token provided to allow your user to reset their password - const resetPasswordURL = `https://yourfrontend.com/reset-password?token=${token}`; + const resetPasswordURL = `https://yourfrontend.com/reset-password?token=${token}` return ` @@ -123,22 +122,21 @@ export const Customers: CollectionConfig = {

- `; - } + ` + }, // highlight-end - } - } -}; + }, + }, +} ``` Important:
- If you specify a different URL to send your users to for resetting their - password, such as a page on the frontend of your app or similar, you need to - handle making the call to the Payload REST or GraphQL reset-password operation - yourself on your frontend, using the token that was provided for you. Above, - it was passed via query parameter. + If you specify a different URL to send your users to for resetting their password, such as a page + on the frontend of your app or similar, you need to handle making the call to the Payload REST or + GraphQL reset-password operation yourself on your frontend, using the token that was provided for + you. Above, it was passed via query parameter.
**`generateEmailSubject`** @@ -173,8 +171,7 @@ Function that accepts one argument, containing `{ req, token, user }`, that allo Example: ```ts -import { CollectionConfig } from 'payload/types'; - +import { CollectionConfig } from 'payload/types' export const Customers: CollectionConfig = { slug: 'customers', @@ -183,24 +180,23 @@ export const Customers: CollectionConfig = { // highlight-start generateEmailHTML: ({ req, token, user }) => { // Use the token provided to allow your user to verify their account - const url = `https://yourfrontend.com/verify?token=${token}`; + const url = `https://yourfrontend.com/verify?token=${token}` - return `Hey ${user.email}, verify your email by clicking here: ${url}`; - } + return `Hey ${user.email}, verify your email by clicking here: ${url}` + }, // highlight-end - } - } -}; + }, + }, +} ``` Important:
- If you specify a different URL to send your users to for email verification, - such as a page on the frontend of your app or similar, you need to handle - making the call to the Payload REST or GraphQL verification operation yourself - on your frontend, using the token that was provided for you. Above, it was - passed via query parameter. + If you specify a different URL to send your users to for email verification, such as a page on the + frontend of your app or similar, you need to handle making the call to the Payload REST or GraphQL + verification operation yourself on your frontend, using the token that was provided for you. + Above, it was passed via query parameter.
**`generateEmailSubject`** @@ -231,9 +227,8 @@ As of Payload `1.0.0`, you can add additional authentication strategies to Paylo Behind the scenes, Payload uses PassportJS to power its local authentication strategy, so most strategies listed on the PassportJS website will work seamlessly. Combined with adding custom components to the admin panel's `Login` view, you can create advanced authentication strategies directly within Payload. - This is an advanced feature, so only attempt this if you are an experienced - developer. Otherwise, just let Payload's built-in authentication handle user - auth for you. + This is an advanced feature, so only attempt this if you are an experienced developer. Otherwise, + just let Payload's built-in authentication handle user auth for you. The `strategies` property is an array that takes objects with the following properties: @@ -250,7 +245,6 @@ However, if you pass a function to `strategy`, `name` is a required property. In either case, Payload will prefix the strategy name with the collection `slug` that the strategy is passed to. - ### Admin autologin For testing and demo purposes you may want to skip forcing the admin user to login in order to access the panel. @@ -259,29 +253,33 @@ The default is that all users will have to login and this should not be enabled #### autoLogin Options -| Option | Description | -| -------------------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **`email`** | The email address of the user to login as | -| **`password`** | The password of the user to login as | -| **`prefillOnly`** | If set to true, the login credentials will be prefilled but the user will still need to click the login button. | +| Option | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------- | +| **`email`** | The email address of the user to login as | +| **`password`** | The password of the user to login as | +| **`prefillOnly`** | If set to true, the login credentials will be prefilled but the user will still need to click the login button. | The recommended way to use this feature is behind an environment variable to ensure it is disabled when in production. **Example:** ```ts - export default buildConfig({ admin: { user: 'users', // highlight-start - autoLogin: process.env.PAYLOAD_PUBLIC_ENABLE_AUTOLOGIN === 'true' ? { - email: 'test@example.com', - password: 'test', - prefillOnly: true, - } : false, + autoLogin: + process.env.PAYLOAD_PUBLIC_ENABLE_AUTOLOGIN === 'true' + ? { + email: 'test@example.com', + password: 'test', + prefillOnly: true, + } + : false, // highlight-end }, - collections: [ /** */], + collections: [ + /** */ + ], }) ``` diff --git a/docs/authentication/operations.mdx b/docs/authentication/operations.mdx index 89c281c1e..e4b571b62 100644 --- a/docs/authentication/operations.mdx +++ b/docs/authentication/operations.mdx @@ -17,6 +17,7 @@ The Access operation returns what a logged in user can and can't do with the col `GET http://localhost:3000/api/access` Example response: + ```ts { canAccessAdmin: true, @@ -77,6 +78,7 @@ Returns either a logged in user with token or null when there is no logged in us `GET http://localhost:3000/api/[collection-slug]/me` Example response: + ```ts { user: { // The JWT "payload" ;) from the logged in user @@ -108,6 +110,7 @@ query { Accepts an `email` and `password`. On success, it will return the logged in user as well as a token that can be used to authenticate. In the GraphQL and REST APIs, this operation also automatically sets an HTTP-only cookie including the user's token. If you pass an Express `res` to the Local API operation, Payload will set a cookie there as well. **Example REST API login**: + ```ts const res = await fetch('http://localhost:3000/api/[collection-slug]/login', { method: 'POST', @@ -117,10 +120,10 @@ const res = await fetch('http://localhost:3000/api/[collection-slug]/login', { body: JSON.stringify({ email: 'dev@payloadcms.com', password: 'this-is-not-our-password...or-is-it?', - }) + }), }) -const json = await res.json(); +const json = await res.json() // JSON will be equal to the following: /* @@ -168,6 +171,7 @@ const result = await payload.login({ As Payload sets HTTP-only cookies, logging out cannot be done by just removing a cookie in JavaScript, as HTTP-only cookies are inaccessible by JS within the browser. So, Payload exposes a `logout` operation to delete the token in a safe way. **Example REST API logout**: + ```ts const res = await fetch('http://localhost:3000/api/[collection-slug]/logout', { method: 'POST', @@ -194,6 +198,7 @@ This operation requires a non-expired token to send back a new one. If the user' If successful, this operation will automatically renew the user's HTTP-only cookie and will send back the updated token in JSON. **Example REST API token refresh**: + ```ts const res = await fetch('http://localhost:3000/api/[collection-slug]/refresh-token', { method: 'POST', @@ -202,7 +207,7 @@ const res = await fetch('http://localhost:3000/api/[collection-slug]/refresh-tok }, }) -const json = await res.json(); +const json = await res.json() // JSON will be equal to the following: /* @@ -233,7 +238,10 @@ mutation { ``` - The Refresh operation will automatically find the user's token in either a JWT header or the HTTP-only cookie. But, you can specify the token you're looking to refresh by providing the REST API with a `token` within the JSON body of the request, or by providing the GraphQL resolver a `token` arg. + The Refresh operation will automatically find the user's token in either a JWT header or the + HTTP-only cookie. But, you can specify the token you're looking to refresh by providing the REST + API with a `token` within the JSON body of the request, or by providing the GraphQL resolver a + `token` arg. ### Verify by Email @@ -241,13 +249,14 @@ mutation { If your collection supports email verification, the Verify operation will be exposed which accepts a verification token and sets the user's `_verified` property to `true`, thereby allowing the user to authenticate with the Payload API. **Example REST API user verification**: + ```ts const res = await fetch(`http://localhost:3000/api/[collection-slug]/verify/${TOKEN_HERE}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, -}); +}) ``` **Example GraphQL Mutation**: @@ -274,6 +283,7 @@ If a user locks themselves out and you wish to deliberately unlock them, you can To restrict who is allowed to unlock users, you can utilize the [`unlock`](/docs/access-control/overview#unlock) access control function. **Example REST API unlock**: + ```ts const res = await fetch(`http://localhost:3000/api/[collection-slug]/unlock`, { method: 'POST', @@ -308,6 +318,7 @@ The link to reset the user's password contains a token which is what allows the By default, the Forgot Password operations send users to the Payload Admin panel to reset their password, but you can customize the generated email to send users to the frontend of your app instead by [overriding the email HTML](/docs/authentication/config#forgot-password). **Example REST API Forgot Password**: + ```ts const res = await fetch(`http://localhost:3000/api/[collection-slug]/forgot-password`, { method: 'POST', @@ -317,7 +328,7 @@ const res = await fetch(`http://localhost:3000/api/[collection-slug]/forgot-pass body: JSON.stringify({ email: 'dev@payloadcms.com', }), -}); +}) ``` **Example GraphQL Mutation**: @@ -336,13 +347,18 @@ const token = await payload.forgotPassword({ data: { email: 'dev@payloadcms.com', }, - disableEmail: false // you can disable the auto-generation of email via local API -}); + disableEmail: false, // you can disable the auto-generation of email via local API +}) ``` - Tip:
- You can stop the reset-password email from being sent via using the local API. This is helpful if you need to create user accounts programmatically, but not set their password for them. This effectively generates a reset password token which you can then use to send to a page you create, allowing a user to "complete" their account by setting their password. In the background, you'd use the token to "reset" their password. + Tip: +
+ You can stop the reset-password email from being sent via using the local API. This is helpful if + you need to create user accounts programmatically, but not set their password for them. This + effectively generates a reset password token which you can then use to send to a page you create, + allowing a user to "complete" their account by setting their password. In the background, you'd + use the token to "reset" their password.
### Reset Password @@ -350,6 +366,7 @@ const token = await payload.forgotPassword({ After a user has "forgotten" their password and a token is generated, that token can be used to send to the reset password operation along with a new password which will allow the user to reset their password securely. **Example REST API Reset Password**: + ```ts const res = await fetch(`http://localhost:3000/api/[collection-slug]/reset-password`, { method: 'POST', diff --git a/docs/authentication/overview.mdx b/docs/authentication/overview.mdx index 486c9ab3b..3730407e8 100644 --- a/docs/authentication/overview.mdx +++ b/docs/authentication/overview.mdx @@ -12,13 +12,14 @@ keywords: authentication, config, configuration, overview, documentation, Conten /> - Payload provides for highly secure and customizable user Authentication out of the box, which allows for users to identify themselves to Payload. + Payload provides for highly secure and customizable user Authentication out of the box, which + allows for users to identify themselves to Payload. Authentication is used within the Payload Admin panel itself as well as throughout your app(s) themselves however you determine necessary. ![Authentication admin panel functionality](https://payloadcms.com/images/docs/auth-admin.jpg) -*Admin panel screenshot depicting an Admins Collection with Auth enabled* +_Admin panel screenshot depicting an Admins Collection with Auth enabled_ **Here are some common use cases of Authentication outside of Payload's dashboard itself:** @@ -38,7 +39,7 @@ Every Payload Collection can opt-in to supporting Authentication by specifying t Simple example collection: ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const Admins: CollectionConfig = { slug: 'admins', @@ -56,13 +57,8 @@ export const Admins: CollectionConfig = { name: 'role', type: 'select', required: true, - options: [ - 'user', - 'admin', - 'editor', - 'developer', - ], - } + options: ['user', 'admin', 'editor', 'developer'], + }, ], } ``` @@ -86,8 +82,11 @@ Successfully logging in returns a `JWT` (JSON web token) which is how a user wil You can specify what data gets encoded to the JWT token by setting `saveToJWT` to true in your auth collection fields. If you wish to use a different key other than the field `name`, you can provide it to `saveToJWT` as a string. It is also possible to use `saveToJWT` on fields that are nested in inside groups and tabs. If a group has a `saveToJWT` set it will include the object with all sub-fields in the token. You can set `saveToJWT: false` for any fields you wish to omit. If a field inside a group has `saveToJWT` set, but the group does not, the field will be included at the top level of the token. - Tip:
- You can access the logged-in user from access control functions and hooks via the Express req. The logged-in user is automatically added as the user property. + Tip: +
+ You can access the logged-in user from access control functions and hooks via the Express{' '} + req. The logged-in user is automatically added as the user{' '} + property.
### HTTP-only cookies @@ -107,16 +106,19 @@ Fetch example, including credentials: ```ts const response = await fetch('http://localhost:3000/api/pages', { credentials: 'include', -}); +}) -const pages = await response.json(); +const pages = await response.json() ``` For more about how to automatically include cookies in requests from your app to your Payload API, [click here](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Sending_a_request_with_credentials_included). - Tip:
- To make sure you have a Payload cookie set properly in your browser after logging in, you can use Chrome's Developer Tools - Application - Cookies - [your-domain-here]. The Chrome Developer tools will still show HTTP-only cookies, even when JavaScript running on the page can't. + Tip: +
+ To make sure you have a Payload cookie set properly in your browser after logging in, you can use + Chrome's Developer Tools - Application - Cookies - [your-domain-here]. The Chrome Developer tools + will still show HTTP-only cookies, even when JavaScript running on the page can't.
### CSRF Protection @@ -128,28 +130,33 @@ For example, let's say you have a very popular app running at coolsite.com. This So, if a user of coolsite.com is logged in and just browsing around on the internet, they might stumble onto a page with bad intentions. That bad page might automatically make requests to all sorts of sites to see if they can find one that they can log into - and coolsite.com might be on their list. If your user was logged in while they visited that evil site, the attacker could do whatever they wanted as if they were your coolsite.com user by just sending requests to the coolsite API (which would automatically include the auth cookie). They could send themselves a bunch of money from your user's account, change the user's password, etc. This is what a CSRF attack is. - To protect against CSRF attacks, Payload only accepts cookie-based authentication from domains that you explicitly whitelist. + + To protect against CSRF attacks, Payload only accepts cookie-based authentication from domains + that you explicitly whitelist. + To define domains that should allow users to identify themselves via the Payload HTTP-only cookie, use the `csrf` option on the base Payload config to whitelist domains that you trust. `payload.config.ts`: + ```ts -import { buildConfig } from 'payload/config'; +import { buildConfig } from 'payload/config' const config = buildConfig({ collections: [ // collections here ], // highlight-start - csrf: [ // whitelist of domains to allow cookie auth from + csrf: [ + // whitelist of domains to allow cookie auth from 'https://your-frontend-app.com', 'https://your-other-frontend-app.com', ], // highlight-end -}); +}) -export default config; +export default config ``` ### Identifying users via the Authorization Header @@ -157,11 +164,12 @@ export default config; In addition to authenticating via an HTTP-only cookie, you can also identify users via the `Authorization` header on an HTTP request. Example: + ```ts const request = await fetch('http://localhost:3000', { headers: { - Authorization: `JWT ${token}` - } + Authorization: `JWT ${token}`, + }, }) ``` diff --git a/docs/authentication/using-middleware.mdx b/docs/authentication/using-middleware.mdx index 945c0a735..c4e7f35be 100644 --- a/docs/authentication/using-middleware.mdx +++ b/docs/authentication/using-middleware.mdx @@ -11,48 +11,47 @@ Because Payload uses your existing Express server, you are free to add whatever This approach has a ton of benefits - it's great for isolation of concerns and limiting scope, but it also means that your additional routes won't have access to Payload's user authentication. - You can make full use of Payload's built-in authentication within your own - custom Express endpoints by adding Payload's authentication middleware. + You can make full use of Payload's built-in authentication within your own custom Express + endpoints by adding Payload's authentication middleware. - Payload must be initialized before the `payload.authenticate` middleware can - be used. This is done by calling `payload.init()` prior to adding the - middleware. + Payload must be initialized before the `payload.authenticate` middleware can be used. This is done + by calling `payload.init()` prior to adding the middleware. Example in `server.js`: ```ts -import express from "express"; -import payload from "payload"; +import express from 'express' +import payload from 'payload' -const app = express(); +const app = express() const start = async () => { await payload.init({ - secret: "PAYLOAD_SECRET_KEY", - mongoURL: "mongodb://localhost/payload", + secret: 'PAYLOAD_SECRET_KEY', + mongoURL: 'mongodb://localhost/payload', express: app, - }); + }) - const router = express.Router(); + const router = express.Router() // Note: Payload must be initialized before the `payload.authenticate` middleware can be used - router.use(payload.authenticate); // highlight-line + router.use(payload.authenticate) // highlight-line - router.get("/", (req, res) => { + router.get('/', (req, res) => { if (req.user) { - return res.send(`Authenticated successfully as ${req.user.email}.`); + return res.send(`Authenticated successfully as ${req.user.email}.`) } - return res.send("Not authenticated"); - }); + return res.send('Not authenticated') + }) - app.use("/some-route-here", router); + app.use('/some-route-here', router) - app.listen(3000); -}; + app.listen(3000) +} -start(); +start() ``` diff --git a/docs/cloud/configuration.mdx b/docs/cloud/configuration.mdx index 7e836805d..161409dae 100644 --- a/docs/cloud/configuration.mdx +++ b/docs/cloud/configuration.mdx @@ -11,10 +11,10 @@ keywords: configuration, config, settings, project, cloud, payload cloud, deploy Once you have created a project, you will need to select your plan. This will determine the resources that are allocated to your project and the features that are available to you. - Note: All Payload Cloud teams that deploy a project require a card on file. - This helps us prevent fraud and abuse on our platform. If you select a plan - with a free trial, you will not be charged until your trial period is over. - We’ll remind you 7 days before your trial ends and you can cancel anytime. + Note: All Payload Cloud teams that deploy a project require a card on file. This helps us prevent + fraud and abuse on our platform. If you select a plan with a free trial, you will not be charged + until your trial period is over. We’ll remind you 7 days before your trial ends and you can cancel + anytime. ### Project Details @@ -44,8 +44,8 @@ If you are deploying a new project from a template, the following settings will Any of the features in Payload Cloud that require environment variables will automatically be provided to your application. If your app requires any custom environment variables, you can set them here. - Note: For security reasons, any variables you wish to provide to the Admin - panel must be prefixed with `PAYLOAD_PUBLIC_`.  Learn more + Note: For security reasons, any variables you wish to provide to the Admin panel must be prefixed + with `PAYLOAD_PUBLIC_`.  Learn more [here](https://payloadcms.com/docs/admin/webpack#admin-environment-vars). @@ -54,9 +54,8 @@ Any of the features in Payload Cloud that require environment variables will aut Payment methods can be set per project and can be updated any time. You can use team’s default payment method, or add a new one. Modify your payment methods in your Project settings / Team settings. - Note: All Payload Cloud teams that deploy a project require a - card on file. This helps us prevent fraud and abuse on our platform. If you - select a plan with a free trial, you will not be charged until your trial - period is over. We’ll remind you 7 days before your trial ends and you can - cancel anytime. + Note: All Payload Cloud teams that deploy a project require a card on file. This + helps us prevent fraud and abuse on our platform. If you select a plan with a free trial, you will + not be charged until your trial period is over. We’ll remind you 7 days before your trial ends and + you can cancel anytime. diff --git a/docs/cloud/creating-a-project.mdx b/docs/cloud/creating-a-project.mdx index b8fdaa302..a315bd996 100644 --- a/docs/cloud/creating-a-project.mdx +++ b/docs/cloud/creating-a-project.mdx @@ -13,9 +13,8 @@ Payload Cloud offers various plans tailored to meet your specific needs, includi To get started, you first need to create an account. Head over to [the login screen](https://payloadcms.com/login) and **Register for Free**. - To create your first project, you can either select [a - template](#starting-from-a-template) or [import an existing - project](#importing-from-an-existing-codebase) from GitHub. + To create your first project, you can either select [a template](#starting-from-a-template) or + [import an existing project](#importing-from-an-existing-codebase) from GitHub. ## Starting from a Template @@ -32,9 +31,8 @@ Next, select your `GitHub Scope`. If you belong to multiple organizations, they After selecting your scope, create a unique `repository name` and select whether you want your repository to be public or private on GitHub. - Note: Public repositories can be accessed by anyone online, - while private repositories grant access only to you and anyone you explicitly - authorize. + Note: Public repositories can be accessed by anyone online, while private + repositories grant access only to you and anyone you explicitly authorize. Once you are ready, click **Create Project**. This will clone the selected template to a new repository in your GitHub account, and take you to the configuration page to set up your project for deployment. @@ -47,7 +45,7 @@ Payload Cloud works for any Node.js + MongoDB app. From the New Project page, se _Creating a new project from an existing repository._ - Note: In order to make use of the features of Payload Cloud - in your own codebase, you will need to add the [Cloud - Plugin](https://github.com/payloadcms/plugin-cloud) to your Payload app. + Note: In order to make use of the features of Payload Cloud in your own codebase, + you will need to add the [Cloud Plugin](https://github.com/payloadcms/plugin-cloud) to your + Payload app. diff --git a/docs/cloud/projects.mdx b/docs/cloud/projects.mdx index 3fc72b5f9..4fe0c7508 100644 --- a/docs/cloud/projects.mdx +++ b/docs/cloud/projects.mdx @@ -9,11 +9,10 @@ keywords: cloud, payload cloud, projects, project, overview, database, file stor ### Overview - The overview tab shows your most recent deployment, along with build and - deployment logs. From here, you can see your live URL, deployment details like - timestamps and commit hash, as well as the status of your deployment. You can - also trigger a redeployment manually, which will rebuild your project using - the current configuration. + The overview tab shows your most recent deployment, along with build and deployment logs. From + here, you can see your live URL, deployment details like timestamps and commit hash, as well as + the status of your deployment. You can also trigger a redeployment manually, which will rebuild + your project using the current configuration. ![Payload Cloud Overview Page](https://payloadcms.com/images/docs/cloud/overview-page.jpg) @@ -40,8 +39,8 @@ You can update settings from your Project’s Settings tab. Changes to your buil From the Environment Variables page of the Settings tab, you can add, update and delete variables for use in your project. Like build settings, these changes will trigger a redeployment of your project. - Note: For security reasons, any variables you wish to provide to the Admin - panel must be prefixed with `PAYLOAD_PUBLIC_`.  Learn more + Note: For security reasons, any variables you wish to provide to the Admin panel must be prefixed + with `PAYLOAD_PUBLIC_`.  Learn more [here](https://payloadcms.com/docs/admin/webpack#admin-environment-vars). ### Custom Domains @@ -49,9 +48,8 @@ From the Environment Variables page of the Settings tab, you can add, update and With Payload Cloud, you can add custom domain names to your project. To do so, first go to the Domains page of the Settings tab of your project. Here you can see your default domain. To add a new domain, type in the domain name you wish to use. - Note: do not include the protocol (http:// or https://) or any routes (/page). - Only include the domain name and extension, and optionally a subdomain. - - your-domain.com - backend.your-domain.com + Note: do not include the protocol (http:// or https://) or any routes (/page). Only include the + domain name and extension, and optionally a subdomain. - your-domain.com - backend.your-domain.com Once you click save, a DNS record will be generated for your domain name to point to your live project. Add this record into your DNS provider’s records, and once the records are resolving properly (this can take 1hr to 48hrs in some cases), your domain will now to point to your live project. @@ -60,9 +58,9 @@ You will also need to configure your Payload project to use your specified domai ```ts export default buildConfig({ - serverURL: "https://example.com", + serverURL: 'https://example.com', // the rest of your config, -}); +}) ``` ### Email @@ -84,18 +82,18 @@ Projects generated from a template will come pre-configured with the official Cl `yarn add @payloadcms/plugin-cloud` ```js -import { payloadCloud } from "@payloadcms/plugin-cloud"; -import { buildConfig } from "payload/config"; +import { payloadCloud } from '@payloadcms/plugin-cloud' +import { buildConfig } from 'payload/config' export default buildConfig({ plugins: [payloadCloud()], // rest of config -}); +}) ``` - **Note:** If your Payload config already has an email with transport, this - will take precedence over Payload Cloud's email service. + **Note:** If your Payload config already has an email with transport, this will take precedence + over Payload Cloud's email service. ##### **Optional configuration** @@ -106,5 +104,5 @@ If you wish to opt-out of any Payload cloud features, the plugin also accepts op payloadCloud({ storage: false, // Disable file storage email: false, // Disable email delivery -}); +}) ``` diff --git a/docs/cloud/teams.mdx b/docs/cloud/teams.mdx index 80d367cd5..64eb26ab7 100644 --- a/docs/cloud/teams.mdx +++ b/docs/cloud/teams.mdx @@ -7,8 +7,8 @@ keywords: team, teams, billing, subscription, payment, plan, plans, cloud, paylo --- - Within Payload Cloud, the team management feature offers you the ability to - manage your organization, team members, billing, and subscription settings. + Within Payload Cloud, the team management feature offers you the ability to manage your + organization, team members, billing, and subscription settings. ![Payload Cloud Team Settings](https://payloadcms.com/images/docs/cloud/team-settings.jpg) diff --git a/docs/configuration/collections.mdx b/docs/configuration/collections.mdx index 6b6f3c028..035f39b50 100644 --- a/docs/configuration/collections.mdx +++ b/docs/configuration/collections.mdx @@ -12,49 +12,49 @@ It's often best practice to write your Collections in separate files and then im ## Options -| Option | Description | -|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Collection. | -| **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Collection. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. | -| **`indexes`** * | Array of database indexes to create, including compound indexes that have multiple fields. | -| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. | -| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). | -| **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More](/docs/hooks/overview#collection-hooks) | -| **`access`** | Provide access control functions to define exactly who should be able to do what with Documents in this Collection. [More](/docs/access-control/overview/#collections) | -| **`auth`** | Specify options if you would like this Collection to feature authentication. For more, consult the [Authentication](/docs/authentication/config) documentation. | -| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](/docs/upload/overview) documentation. | -| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. | -| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#collection-config) | -| **`endpoints`** | Add custom routes to the REST API. Set to `false` to disable routes. [More](/docs/rest-api/overview#custom-endpoints) | -| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. Set to `false` to disable GraphQL. | -| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. | -| **`defaultSort`** | Pass a top-level field to sort by default in the collection List view. Prefix the name of the field with a minus symbol ("-") to sort in descending order. | -| **`pagination`** | Set pagination-specific options for this collection. [More](#pagination) | -| **`custom`** | Extension point for adding custom data (e.g. for plugins) | +| Option | Description | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Collection. | +| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. | +| **`indexes`** \* | Array of database indexes to create, including compound indexes that have multiple fields. | +| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. | +| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). | +| **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More](/docs/hooks/overview#collection-hooks) | +| **`access`** | Provide access control functions to define exactly who should be able to do what with Documents in this Collection. [More](/docs/access-control/overview/#collections) | +| **`auth`** | Specify options if you would like this Collection to feature authentication. For more, consult the [Authentication](/docs/authentication/config) documentation. | +| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](/docs/upload/overview) documentation. | +| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. | +| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#collection-config) | +| **`endpoints`** | Add custom routes to the REST API. Set to `false` to disable routes. [More](/docs/rest-api/overview#custom-endpoints) | +| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. Set to `false` to disable GraphQL. | +| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. | +| **`defaultSort`** | Pass a top-level field to sort by default in the collection List view. Prefix the name of the field with a minus symbol ("-") to sort in descending order. | +| **`pagination`** | Set pagination-specific options for this collection. [More](#pagination) | +| **`custom`** | Extension point for adding custom data (e.g. for plugins) | -*\* An asterisk denotes that a property is required.* +_\* An asterisk denotes that a property is required._ #### Simple collection example ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const Orders: CollectionConfig = { - slug: 'orders', - fields: [ - { - name: 'total', - type: 'number', - required: true, - }, - { - name: 'placedBy', - type: 'relationship', - relationTo: 'customers', - required: true, - } - ], -}; + slug: 'orders', + fields: [ + { + name: 'total', + type: 'number', + required: true, + }, + { + name: 'placedBy', + type: 'relationship', + relationTo: 'customers', + required: true, + }, + ], +} ``` #### More collection config examples @@ -66,7 +66,7 @@ You can find an assortment of [example collection configs](https://github.com/pa You can customize the way that the Admin panel behaves on a collection-by-collection basis by defining the `admin` property on a collection's config. | Option | Description | -|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `group` | Text used as a label for grouping collection and global links together in the navigation. | | `hidden` | Set to true or a function, called with the current user, returning true to exclude this collection from navigation and admin routing. | | `hooks` | Admin-specific hooks for this collection. [More](#admin-hooks) | @@ -95,37 +95,37 @@ If the function is specified, a Preview button will automatically appear in the **Example collection with preview function:** ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const Posts: CollectionConfig = { - slug: 'posts', - fields: [ - { - name: 'slug', - type: 'text', - required: true, - }, - ], - admin: { - preview: (doc, { locale }) => { - if (doc?.slug) { - return `https://bigbird.com/preview/posts/${doc.slug}?locale=${locale}`; - } + slug: 'posts', + fields: [ + { + name: 'slug', + type: 'text', + required: true, + }, + ], + admin: { + preview: (doc, { locale }) => { + if (doc?.slug) { + return `https://bigbird.com/preview/posts/${doc.slug}?locale=${locale}` + } - return null; - }, - }, -}; + return null + }, + }, +} ``` ### Pagination Here are a few options that you can specify options for pagination on a collection-by-collection basis: -| Option | Description | -| --------------------------- | -------------| -| `defaultLimit` | Integer that specifies the default per-page limit that should be used. Defaults to 10. | -| `limits` | Provide an array of integers to use as per-page options for admins to choose from in the List view. | +| Option | Description | +| -------------- | --------------------------------------------------------------------------------------------------- | +| `defaultLimit` | Integer that specifies the default per-page limit that should be used. Defaults to 10. | +| `limits` | Provide an array of integers to use as per-page options for admins to choose from in the List view. | ### Access control @@ -146,8 +146,10 @@ In the List view, there is a "search" box that allows you to quickly find a docu For example, let's say you have a Posts collection with `title`, `metaDescription`, and `tags` fields - and you want all three of those fields to be searchable in the List view. You can simply add `admin.listSearchableFields: ['title', 'metaDescription', 'tags']` - and the admin UI will automatically search on those three fields plus the ID field. - Note:
- If you are adding listSearchableFields, make sure you index each of these fields so your admin queries can remain performant. + Note: +
+ If you are adding listSearchableFields, make sure you index each of these fields + so your admin queries can remain performant.
### Admin Hooks @@ -161,24 +163,24 @@ The `beforeDuplicate` hook is an async function that accepts an object containin Example: ```ts -import { BeforeDuplicate, CollectionConfig } from 'payload/types'; +import { BeforeDuplicate, CollectionConfig } from 'payload/types' // Your auto-generated Page type -import { Page } from '../payload-types.ts'; +import { Page } from '../payload-types.ts' const beforeDuplicate: BeforeDuplicate = ({ data }) => { return { ...data, title: `${data.title} Copy`, uniqueField: data.uniqueField ? `${data.uniqueField}-copy` : '', - }; -}; + } +} export const Page: CollectionConfig = { slug: 'pages', admin: { hooks: { beforeDuplicate, - } + }, }, fields: [ { @@ -189,8 +191,8 @@ export const Page: CollectionConfig = { name: 'uniqueField', type: 'text', unique: true, - } - ] + }, + ], } ``` @@ -199,14 +201,14 @@ export const Page: CollectionConfig = { You can import collection types as follows: ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' // This is the type used for incoming collection configs. // Only the bare minimum properties are marked as required. ``` ```ts -import { SanitizedCollectionConfig } from 'payload/types'; +import { SanitizedCollectionConfig } from 'payload/types' // This is the type used after an incoming collection config is fully sanitized. // Generally, this is only used internally by Payload. diff --git a/docs/configuration/express.mdx b/docs/configuration/express.mdx index 360db219b..74fc66f3b 100644 --- a/docs/configuration/express.mdx +++ b/docs/configuration/express.mdx @@ -70,11 +70,11 @@ To customize compression options, pass an object to the Payload config's `expres ```js { - express: { - compression: { - // settings go here - } - } + express: { + compression: { + // settings go here + } + } } ``` diff --git a/docs/configuration/globals.mdx b/docs/configuration/globals.mdx index 396b53397..c92da8cc9 100644 --- a/docs/configuration/globals.mdx +++ b/docs/configuration/globals.mdx @@ -32,29 +32,29 @@ _\* An asterisk denotes that a property is required._ #### Simple Global example ```ts -import { GlobalConfig } from "payload/types"; +import { GlobalConfig } from 'payload/types' const Nav: GlobalConfig = { - slug: "nav", + slug: 'nav', fields: [ { - name: "items", - type: "array", + name: 'items', + type: 'array', required: true, maxRows: 8, fields: [ { - name: "page", - type: "relationship", - relationTo: "pages", // "pages" is the slug of an existing collection + name: 'page', + type: 'relationship', + relationTo: 'pages', // "pages" is the slug of an existing collection required: true, }, ], }, ], -}; +} -export default Nav; +export default Nav ``` #### Global config example @@ -66,8 +66,8 @@ You can find an [example Global config](https://github.com/payloadcms/public-dem You can customize the way that the Admin panel behaves on a Global-by-Global basis by defining the `admin` property on a Global's config. | Option | Description | -|--------------|-----------------------------------------------------------------------------------------------------------------------------------| -| `group` | Text used as a label for grouping collection and global links together in the navigation. | +| ------------ | --------------------------------------------------------------------------------------------------------------------------------- | +| `group` | Text used as a label for grouping collection and global links together in the navigation. | | `hidden` | Set to true or a function, called with the current user, returning true to exclude this global from navigation and admin routing. | | `components` | Swap in your own React components to be used within this Global. [More](/docs/admin/components#globals) | | `preview` | Function to generate a preview URL within the Admin panel for this global that can point to your app. [More](#preview). | @@ -87,27 +87,27 @@ If the function is specified, a Preview button will automatically appear in the **Example global with preview function:** ```ts -import { GlobalConfig } from "payload/types"; +import { GlobalConfig } from 'payload/types' export const MyGlobal: GlobalConfig = { - slug: "my-global", + slug: 'my-global', fields: [ { - name: "slug", - type: "text", + name: 'slug', + type: 'text', required: true, }, ], admin: { preview: (doc, { locale }) => { if (doc?.slug) { - return `https://bigbird.com/preview/${doc.slug}?locale=${locale}`; + return `https://bigbird.com/preview/${doc.slug}?locale=${locale}` } - return null; + return null }, }, -}; +} ``` ### Access control @@ -127,14 +127,14 @@ Globals support all field types that Payload has to offer—including simple fie You can import global types as follows: ```ts -import { GlobalConfig } from "payload/types"; +import { GlobalConfig } from 'payload/types' // This is the type used for incoming global configs. // Only the bare minimum properties are marked as required. ``` ```ts -import { SanitizedGlobalConfig } from "payload/types"; +import { SanitizedGlobalConfig } from 'payload/types' // This is the type used after an incoming global config is fully sanitized. // Generally, this is only used internally by Payload. diff --git a/docs/configuration/i18n.mdx b/docs/configuration/i18n.mdx index 2c39244d7..d699e58ad 100644 --- a/docs/configuration/i18n.mdx +++ b/docs/configuration/i18n.mdx @@ -13,47 +13,47 @@ While Payload's built-in features come translated, you may want to also translat Here is an example of a simple collection supporting both English and Spanish editors: ```ts -import { CollectionConfig } from "payload/types"; +import { CollectionConfig } from 'payload/types' export const Articles: CollectionConfig = { - slug: "articles", + slug: 'articles', labels: { singular: { - en: "Article", - es: "Artículo", + en: 'Article', + es: 'Artículo', }, plural: { - en: "Articles", - es: "Artículos", + en: 'Articles', + es: 'Artículos', }, }, admin: { - group: { en: "Content", es: "Contenido" }, + group: { en: 'Content', es: 'Contenido' }, }, fields: [ { - name: "title", - type: "text", + name: 'title', + type: 'text', label: { - en: "Title", - es: "Título", + en: 'Title', + es: 'Título', }, admin: { - placeholder: { en: "Enter title", es: "Introduce el título" }, + placeholder: { en: 'Enter title', es: 'Introduce el título' }, }, }, { - name: "type", - type: "radio", + name: 'type', + type: 'radio', options: [ { - value: "news", - label: { en: "News", es: "Noticias" }, + value: 'news', + label: { en: 'News', es: 'Noticias' }, }, // etc... ], }, ], -}; +} ``` ### Admin UI @@ -62,8 +62,10 @@ The Payload admin panel reads the language settings of a user's browser and disp After a user logs in, they can change their language selection in the `/account` view. - Note:
- If there is a language that Payload does not yet support, we accept code [contributions](https://github.com/payloadcms/payload/blob/master/contributing.md). + Note: +
+ If there is a language that Payload does not yet support, we accept code + [contributions](https://github.com/payloadcms/payload/blob/master/contributing.md).
### Node Express @@ -81,28 +83,28 @@ In your Payload config, you can add translations and customize the settings in ` **Example Payload config extending i18n:** ```ts -import { buildConfig } from "payload/config"; +import { buildConfig } from 'payload/config' export default buildConfig({ //... i18n: { - fallbackLng: "en", // default + fallbackLng: 'en', // default debug: false, // default resources: { en: { custom: { // namespace can be anything you want - key1: "Translation with {{variable}}", // translation + key1: 'Translation with {{variable}}', // translation }, // override existing translation keys general: { - dashboard: "Home", + dashboard: 'Home', }, }, }, }, //... -}); +}) ``` See the i18next [configuration options](https://www.i18next.com/overview/configuration-options) to learn more. diff --git a/docs/configuration/localization.mdx b/docs/configuration/localization.mdx index 6b92a0a8c..2a052b7c1 100644 --- a/docs/configuration/localization.mdx +++ b/docs/configuration/localization.mdx @@ -22,15 +22,11 @@ export default buildConfig({ // collections go here ], localization: { - locales: [ - 'en', - 'es', - 'de', - ], + locales: ['en', 'es', 'de'], defaultLocale: 'en', fallback: true, }, -}); +}) ``` **Example Payload config set up for localization with full locales objects:** @@ -43,22 +39,22 @@ export default buildConfig({ // collections go here ], localization: { - locales: [ - { - label: "English", - code: "en", - }, - { - label: "Arabic", - code: "ar", - // opt-in to setting default text-alignment on Input fields to rtl (right-to-left) when current locale is rtl - rtl: true, - }, - ], - defaultLocale: "en", - fallback: true, + locales: [ + { + label: 'English', + code: 'en', + }, + { + label: 'Arabic', + code: 'ar', + // opt-in to setting default text-alignment on Input fields to rtl (right-to-left) when current locale is rtl + rtl: true, + }, + ], + defaultLocale: 'en', + fallback: true, }, -}); +}) ``` **Here is a brief explanation of each of the options available within the `localization` property:** @@ -96,17 +92,21 @@ With the above configuration, the `title` field will now be saved in the databas All field types with a `name` property support the `localized` property—even the more complex field types like `array`s and `block`s. - Note:
- Enabling localization for field types that support nested fields will automatically create localized "sets" of all fields contained within the field. For example, if you have a page layout using a blocks field type, you have the choice of either localizing the full layout, by enabling localization on the top-level blocks field, or only certain fields within the layout. + Note: +
+ Enabling localization for field types that support nested fields will automatically create + localized "sets" of all fields contained within the field. For example, if you have a page layout + using a blocks field type, you have the choice of either localizing the full layout, by enabling + localization on the top-level blocks field, or only certain fields within the layout.
Important:
- When converting an existing field to or from `localized: true` the data - structure in the document will change for this field and so existing data for - this field will be lost. Before changing the localization setting on fields - with existing data, you may need to consider a field migration strategy. + When converting an existing field to or from `localized: true` the data structure in the document + will change for this field and so existing data for this field will be lost. Before changing the + localization setting on fields with existing data, you may need to consider a field migration + strategy.
### Retrieving localized docs @@ -152,7 +152,9 @@ query { ``` - In GraphQL, specifying the locale at the top level of a query will automatically apply it throughout all nested relationship fields. You can override this behavior by re-specifying locale arguments in nested related document queries. + In GraphQL, specifying the locale at the top level of a query will automatically apply it + throughout all nested relationship fields. You can override this behavior by re-specifying locale + arguments in nested related document queries. ##### Local API @@ -170,5 +172,9 @@ const posts = await payload.find({ ``` - Tip:
The REST and Local APIs can return all localization data in one request by passing 'all' or '*' as the locale parameter. The response will be structured so that field values come back as the full objects keyed for each locale instead of the single, translated value. + Tip: +
+ The REST and Local APIs can return all localization data in one request by passing 'all' or '*' as + the locale parameter. The response will be structured so that field values come + back as the full objects keyed for each locale instead of the single, translated value.
diff --git a/docs/configuration/overview.mdx b/docs/configuration/overview.mdx index 0c6f3e1f6..8f2b87975 100644 --- a/docs/configuration/overview.mdx +++ b/docs/configuration/overview.mdx @@ -13,8 +13,8 @@ Payload is a _config-based_, code-first CMS and application framework. The Paylo Important:
- This file is included in the Payload admin bundle, so make sure you do not - embed any sensitive information. + This file is included in the Payload admin bundle, so make sure you do not embed any sensitive + information.
## Options @@ -48,21 +48,21 @@ Payload is a _config-based_, code-first CMS and application framework. The Paylo #### Simple example ```ts -import { buildConfig } from "payload/config"; +import { buildConfig } from 'payload/config' export default buildConfig({ collections: [ { - slug: "pages", + slug: 'pages', fields: [ { - name: "title", - type: "text", + name: 'title', + type: 'text', required: true, }, { - name: "content", - type: "richText", + name: 'content', + type: 'richText', required: true, }, ], @@ -70,23 +70,23 @@ export default buildConfig({ ], globals: [ { - slug: "header", + slug: 'header', fields: [ { - name: "nav", - type: "array", + name: 'nav', + type: 'array', fields: [ { - name: "page", - type: "relationship", - relationTo: "pages", + name: 'page', + type: 'relationship', + relationTo: 'pages', }, ], }, ], }, ], -}); +}) ``` #### Full example config @@ -120,10 +120,9 @@ project-name Important:
- If you use an environment variable to configure any properties that are - required for the Admin panel to function (ex. serverURL or any routes), you - need to make sure that your Admin panel code can access it. [Click - here](/docs/admin/webpack#admin-environment-vars) for more info. + If you use an environment variable to configure any properties that are required for the Admin + panel to function (ex. serverURL or any routes), you need to make sure that your Admin panel code + can access it. [Click here](/docs/admin/webpack#admin-environment-vars) for more info.
### Customizing & Automating Config Location Detection @@ -145,7 +144,7 @@ In addition to the above automated detection, you can specify your own location ```json { "scripts": { - "dev": "PAYLOAD_CONFIG_PATH=path/to/custom-config.js node server.js", + "dev": "PAYLOAD_CONFIG_PATH=path/to/custom-config.js node server.js" } } ``` @@ -161,14 +160,14 @@ Payload comes with `isomorphic-fetch` installed which means that even in Node, y You can import config types as follows: ```ts -import { Config } from "payload/config"; +import { Config } from 'payload/config' // This is the type used for an incoming Payload config. // Only the bare minimum properties are marked as required. ``` ```ts -import { SanitizedConfig } from "payload/config"; +import { SanitizedConfig } from 'payload/config' // This is the type used after an incoming Payload config is fully sanitized. // Generally, this is only used internally by Payload. diff --git a/docs/database/overview.mdx b/docs/database/overview.mdx index a8dcf5780..d6e18c653 100644 --- a/docs/database/overview.mdx +++ b/docs/database/overview.mdx @@ -23,7 +23,8 @@ Database transactions allow your application to make a series of database change By default, Payload will use transactions for all operations, as long as it is supported by the configured database. Database changes are contained within all Payload operations and any errors thrown will result in all changes being rolled back without being committed. When transactions are not supported by the database, Payload will continue to operate as expected without them. - Note:
+ Note: +
MongoDB requires a connection to a replicaset in order to make use of transactions.
@@ -37,12 +38,13 @@ const afterChange: CollectionAfterChangeHook = async ({ req }) => { collection: 'my-slug', data: { some: 'data', - } - }); + }, + }) } ``` ### Async Hooks with Transactions + Since Payload hooks can be async and be written to not await the result, it is possible to have an incorrect success response returned on a request that is rolled back. If you have a hook where you do not `await` the result, then you should **not** pass the `req.transactionID`. ```ts @@ -53,7 +55,7 @@ const afterChange: CollectionAfterChangeHook = async ({ req }) => { collection: 'my-slug', data: { some: 'other data', - } + }, }) // Should this call fail, it will not rollback other changes @@ -65,7 +67,7 @@ const afterChange: CollectionAfterChangeHook = async ({ req }) => { collection: 'my-slug', data: { some: 'other data', - } + }, }) } ``` @@ -82,4 +84,3 @@ The following functions can be used for managing transactions: `payload.db.rollbackTransaction` - Takes the identifier for the transaction, discards any changes. ## Migrations - diff --git a/docs/email/overview.mdx b/docs/email/overview.mdx index 74db97567..ce9767add 100644 --- a/docs/email/overview.mdx +++ b/docs/email/overview.mdx @@ -57,15 +57,16 @@ payload.init({ rejectUnauthorized: false, }, }, - fromName: "hello", - fromAddress: "hello@example.com", + fromName: 'hello', + fromAddress: 'hello@example.com', }, // ... -}); +}) ``` - It is best practice to avoid saving credentials or API keys directly in your code, use [environment variables](/docs/configuration/overview#using-environment-variables-in-your-config). + It is best practice to avoid saving credentials or API keys directly in your code, use + [environment variables](/docs/configuration/overview#using-environment-variables-in-your-config). ### Use an email service @@ -73,10 +74,10 @@ payload.init({ Many third party mail providers are available and offer benefits beyond basic SMTP. As an example your payload init could look this if you wanted to use SendGrid.com though the same approach would work for any other [NodeMailer transports](https://nodemailer.com/transports/) shown here or provided by another third party. ```ts -import payload from "payload"; -import nodemailerSendgrid from "nodemailer-sendgrid"; +import payload from 'payload' +import nodemailerSendgrid from 'nodemailer-sendgrid' -const sendGridAPIKey = process.env.SENDGRID_API_KEY; +const sendGridAPIKey = process.env.SENDGRID_API_KEY payload.init({ ...(sendGridAPIKey @@ -85,12 +86,12 @@ payload.init({ transportOptions: nodemailerSendgrid({ apiKey: sendGridAPIKey, }), - fromName: "Admin", - fromAddress: "admin@example.com", + fromName: 'Admin', + fromAddress: 'admin@example.com', }, } : {}), -}); +}) ``` ### Use a custom NodeMailer transport @@ -98,11 +99,11 @@ payload.init({ To take full control of the mail transport you may wish to use `nodemailer.createTransport()` on your server and provide it to Payload init. ```ts -import payload from "payload"; -import nodemailer from "nodemailer"; +import payload from 'payload' +import nodemailer from 'nodemailer' -const payload = require("payload"); -const nodemailer = require("nodemailer"); +const payload = require('payload') +const nodemailer = require('nodemailer') const transport = await nodemailer.createTransport({ host: process.env.SMTP_HOST, @@ -111,16 +112,16 @@ const transport = await nodemailer.createTransport({ user: process.env.SMTP_USER, pass: process.env.SMTP_PASS, }, -}); +}) payload.init({ email: { - fromName: "Admin", - fromAddress: "admin@example.com", + fromName: 'Admin', + fromAddress: 'admin@example.com', transport, }, // ... -}); +}) ``` ### Sending Mail @@ -136,12 +137,12 @@ To see ethereal credentials, add `logMockCredentials: true` to the email options ```ts payload.init({ email: { - fromName: "Admin", - fromAddress: "admin@example.com", + fromName: 'Admin', + fromAddress: 'admin@example.com', logMockCredentials: true, // Optional }, // ... -}); +}) ``` **Console output when starting payload with a mock email instance and logMockCredentials: true** @@ -160,7 +161,8 @@ payload.init({ The mock email handler is used when payload is started with neither `transport` or `transportOptions` to know how to deliver email. - The randomly generated email account username and password will be different each time the Payload server starts. + The randomly generated email account username and password will be different each time the Payload + server starts. ### Using multiple mail providers diff --git a/docs/fields/array.mdx b/docs/fields/array.mdx index 949b28bb4..52af0a607 100644 --- a/docs/fields/array.mdx +++ b/docs/fields/array.mdx @@ -7,9 +7,8 @@ keywords: array, fields, config, configuration, documentation, Content Managemen --- - The Array field type is used when you need to have a set of "repeating" - fields. It stores an array of objects containing the fields that you define. - Its uses can be simple or highly complex. + The Array field type is used when you need to have a set of "repeating" fields. It stores an array + of objects containing the fields that you define. Its uses can be simple or highly complex. { - return data?.title || `Slide ${String(index).padStart(2, "0")}`; + return data?.title || `Slide ${String(index).padStart(2, '0')}` }, }, }, }, ], -}; +} ``` diff --git a/docs/fields/blocks.mdx b/docs/fields/blocks.mdx index 827581eea..87da3205f 100644 --- a/docs/fields/blocks.mdx +++ b/docs/fields/blocks.mdx @@ -7,10 +7,10 @@ keywords: blocks, fields, config, configuration, documentation, Content Manageme --- - The Blocks field type is incredibly powerful and can be used - as a layout builder as well as to define any other flexible content - model you can think of. It stores an array of objects, where each object must - match the shape of one of your provided block configs. + The Blocks field type is incredibly powerful and can be used as a{' '} + layout builder as well as to define any other flexible content model you can think of. It + stores an array of objects, where each object must match the shape of one of your provided block + configs. Tip:
- Best practice is to define each block config in its own file, and then import - them into your Blocks field as necessary. This way each block config can be - easily shared between fields. For instance, using the "layout builder" - example, you might want to feature a few of the same blocks in a Post - collection as well as a Page collection. Abstracting into their own files - trivializes their reusability. + Best practice is to define each block config in its own file, and then import them into your + Blocks field as necessary. This way each block config can be easily shared between fields. For + instance, using the "layout builder" example, you might want to feature a few of the same blocks + in a Post collection as well as a Page collection. Abstracting into their own files trivializes + their reusability. | Option | Description | @@ -97,7 +96,7 @@ The Admin panel provides each block with a `blockName` field which optionally al `collections/ExampleCollection.js` ```ts -import { Block, CollectionConfig } from 'payload/types'; +import { Block, CollectionConfig } from 'payload/types' const QuoteBlock: Block = { slug: 'Quote', // required @@ -116,7 +115,7 @@ const QuoteBlock: Block = { type: 'text', }, ], -}; +} export const ExampleCollection: CollectionConfig = { slug: 'example-collection', @@ -132,7 +131,7 @@ export const ExampleCollection: CollectionConfig = { ], }, ], -}; +} ``` ### TypeScript @@ -140,5 +139,5 @@ export const ExampleCollection: CollectionConfig = { As you build your own Block configs, you might want to store them in separate files but retain typing accordingly. To do so, you can import and use Payload's `Block` type: ```ts -import type { Block } from 'payload/types'; +import type { Block } from 'payload/types' ``` diff --git a/docs/fields/checkbox.mdx b/docs/fields/checkbox.mdx index 4d3da6ec0..1a2f5765f 100644 --- a/docs/fields/checkbox.mdx +++ b/docs/fields/checkbox.mdx @@ -6,42 +6,41 @@ desc: Checkbox field types allow the developer to save a boolean value in the da keywords: checkbox, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express --- - - The Checkbox field type saves a boolean in the database. - +The Checkbox field type saves a boolean in the database. - ### Config -| Option | Description | -| ---------------- | ----------- | -| **`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. | -| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | -| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | -| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | -| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | -| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) | -| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | -| **`defaultValue`** | Provide data to be used for this field's default value, will default to false if field is also `required`. [More](/docs/fields/overview#default-values) | -| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | -| **`required`** | Require this field to have a value. | -| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. | -| **`custom`** | Extension point for adding custom data (e.g. for plugins) | +| Option | Description | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`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. | +| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | +| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | +| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | +| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | +| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) | +| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | +| **`defaultValue`** | Provide data to be used for this field's default value, will default to false if field is also `required`. [More](/docs/fields/overview#default-values) | +| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | +| **`required`** | Require this field to have a value. | +| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. | +| **`custom`** | Extension point for adding custom data (e.g. for plugins) | -*\* An asterisk denotes that a property is required.* +_\* An asterisk denotes that a property is required._ ### Example `collections/ExampleCollection.ts` + ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', @@ -51,7 +50,7 @@ export const ExampleCollection: CollectionConfig = { type: 'checkbox', // required label: 'Click me to see fanciness', defaultValue: false, - } - ] -}; + }, + ], +} ``` diff --git a/docs/fields/code.mdx b/docs/fields/code.mdx index a9126c904..f8d8a00d8 100644 --- a/docs/fields/code.mdx +++ b/docs/fields/code.mdx @@ -7,39 +7,40 @@ desc: The Code field type will store any string in the Database. Learn how to us keywords: code, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express --- - - The Code field type saves a string in the database, but provides the Admin panel with a code editor styled interface. + + The Code field type saves a string in the database, but provides the Admin panel with a code + editor styled interface. - This field uses the `monaco-react` editor syntax highlighting. ### Config -| Option | Description | -| ---------------- | ----------- | -| **`name`** * | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | -| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. | -| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | -| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | -| **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. | -| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. | -| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | -| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | -| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | -| **`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) | -| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | -| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) | -| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | -| **`required`** | Require this field to have a value. | -| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | -| **`custom`** | Extension point for adding custom data (e.g. for plugins) | +| Option | Description | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | +| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. | +| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | +| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | +| **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. | +| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. | +| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | +| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | +| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | +| **`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) | +| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | +| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) | +| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | +| **`required`** | Require this field to have a value. | +| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | +| **`custom`** | Extension point for adding custom data (e.g. for plugins) | _\* An asterisk denotes that a property is required._ @@ -47,16 +48,17 @@ _\* An asterisk denotes that a property is required._ In addition to the default [field admin config](/docs/fields/overview#admin-config), you can adjust the following properties: -| Option | Description | -| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`language`** | This property can be set to any language listed [here](https://github.com/microsoft/monaco-editor/tree/main/src/basic-languages). | -| **`editorOptions`** | Options that can be passed to the monaco editor, [view the full list](https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IDiffEditorConstructionOptions.html). | +| Option | Description | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`language`** | This property can be set to any language listed [here](https://github.com/microsoft/monaco-editor/tree/main/src/basic-languages). | +| **`editorOptions`** | Options that can be passed to the monaco editor, [view the full list](https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IDiffEditorConstructionOptions.html). | ### Example `collections/ExampleCollection.ts + ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', @@ -66,9 +68,9 @@ export const ExampleCollection: CollectionConfig = { type: 'code', // required required: true, admin: { - language: 'javascript' - } - } - ] -}; + language: 'javascript', + }, + }, + ], +} ``` diff --git a/docs/fields/collapsible.mdx b/docs/fields/collapsible.mdx index a38a73494..5e1b06d3d 100644 --- a/docs/fields/collapsible.mdx +++ b/docs/fields/collapsible.mdx @@ -6,41 +6,43 @@ desc: With the Collapsible field, you can place fields within a collapsible layo keywords: row, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express --- - - The Collapsible field is presentational-only and only affects the Admin panel. By using it, you can place fields within a nice layout component that can be collapsed / expanded. + + The Collapsible field is presentational-only and only affects the Admin panel. By using it, you + can place fields within a nice layout component that can be collapsed / expanded. - ### Config -| Option | Description | -| -------------- | ------------------------------------------------------------------------- | -| **`label`** * | A label to render within the header of the collapsible component. This can be a string, function or react component. Function/components receive `({ data, path })` as args. | -| **`fields`** * | Array of field types to nest within this Collapsible. | -| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | -| **`custom`** | Extension point for adding custom data (e.g. for plugins) | +| Option | Description | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`label`** \* | A label to render within the header of the collapsible component. This can be a string, function or react component. Function/components receive `({ data, path })` as args. | +| **`fields`** \* | Array of field types to nest within this Collapsible. | +| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | +| **`custom`** | Extension point for adding custom data (e.g. for plugins) | -*\* An asterisk denotes that a property is required.* +_\* An asterisk denotes that a property is required._ ### Admin Config In addition to the default [field admin config](/docs/fields/overview#admin-config), you can adjust the following properties: -| Option | Description | -| ---------------------- | ------------------------------- | -| **`initCollapsed`** | Set the initial collapsed state | +| Option | Description | +| ------------------- | ------------------------------- | +| **`initCollapsed`** | Set the initial collapsed state | ### Example `collections/ExampleCollection.ts` + ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', @@ -48,7 +50,8 @@ export const ExampleCollection: CollectionConfig = { { label: ({ data }) => data?.title || 'Untitled', type: 'collapsible', // required - fields: [ // required + fields: [ + // required { name: 'title', type: 'text', @@ -60,7 +63,7 @@ export const ExampleCollection: CollectionConfig = { required: true, }, ], - } - ] -}; + }, + ], +} ``` diff --git a/docs/fields/date.mdx b/docs/fields/date.mdx index e6c2ca936..299e3da12 100644 --- a/docs/fields/date.mdx +++ b/docs/fields/date.mdx @@ -7,15 +7,15 @@ keywords: date, fields, config, configuration, documentation, Content Management --- - The Date field type saves a Date in the database and provides the Admin panel - with a customizable time picker interface. + The Date field type saves a Date in the database and provides the Admin panel with a customizable + time picker interface. - This field uses [`react-datepicker`](https://www.npmjs.com/package/react-datepicker) for the Admin panel component. @@ -75,41 +75,41 @@ When only `pickerAppearance` is set, an equivalent format will be rendered in th `collections/ExampleCollection.ts` ```ts -import { CollectionConfig } from "payload/types"; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { - slug: "example-collection", + slug: 'example-collection', fields: [ { - name: "dateOnly", - type: "date", + name: 'dateOnly', + type: 'date', admin: { date: { - pickerAppearance: "dayOnly", - displayFormat: "d MMM yyy", + pickerAppearance: 'dayOnly', + displayFormat: 'd MMM yyy', }, }, }, { - name: "timeOnly", - type: "date", + name: 'timeOnly', + type: 'date', admin: { date: { - pickerAppearance: "timeOnly", - displayFormat: "h:mm:ss a", + pickerAppearance: 'timeOnly', + displayFormat: 'h:mm:ss a', }, }, }, { - name: "monthOnly", - type: "date", + name: 'monthOnly', + type: 'date', admin: { date: { - pickerAppearance: "monthOnly", - displayFormat: "MMMM yyyy", + pickerAppearance: 'monthOnly', + displayFormat: 'MMMM yyyy', }, }, }, ], -}; +} ``` diff --git a/docs/fields/email.mdx b/docs/fields/email.mdx index 7d36686fd..f2213851b 100644 --- a/docs/fields/email.mdx +++ b/docs/fields/email.mdx @@ -6,37 +6,35 @@ desc: The Email field enforces that the value provided is a valid email address. keywords: email, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express --- - - The Email field enforces that the value provided is a valid email address. - +The Email field enforces that the value provided is a valid email address. - ### Config -| Option | Description | -| ---------------- | ----------- | -| **`name`** * | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | -| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. | -| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | -| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | -| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | -| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | -| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | -| **`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) | -| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | -| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) | -| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | -| **`required`** | Require this field to have a value. | -| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | -| **`custom`** | Extension point for adding custom data (e.g. for plugins) | +| Option | Description | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | +| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. | +| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | +| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | +| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | +| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | +| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | +| **`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) | +| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | +| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) | +| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | +| **`required`** | Require this field to have a value. | +| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | +| **`custom`** | Extension point for adding custom data (e.g. for plugins) | -*\* An asterisk denotes that a property is required.* +_\* An asterisk denotes that a property is required._ ### Admin config @@ -53,8 +51,9 @@ Set this property to a string that will be used for browser autocomplete. ### Example `collections/ExampleCollection.ts` + ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', @@ -64,7 +63,7 @@ export const ExampleCollection: CollectionConfig = { type: 'email', // required label: 'Contact Email Address', required: true, - } - ] -}; + }, + ], +} ``` diff --git a/docs/fields/group.mdx b/docs/fields/group.mdx index b9cbdb432..3c5ca228e 100644 --- a/docs/fields/group.mdx +++ b/docs/fields/group.mdx @@ -7,15 +7,15 @@ keywords: group, fields, config, configuration, documentation, Content Managemen --- - The Group field allows fields to be nested under a common property name. It - also groups fields together visually in the Admin panel. + The Group field allows fields to be nested under a common property name. It also groups fields + together visually in the Admin panel. - ### Config @@ -51,27 +51,27 @@ Set this property to `true` to hide this field's gutter within the admin panel. `collections/ExampleCollection.ts` ```ts -import { CollectionConfig } from "payload/types"; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { - slug: "example-collection", + slug: 'example-collection', fields: [ { - name: "pageMeta", // required - type: "group", // required - interfaceName: "Meta", // optional + name: 'pageMeta', // required + type: 'group', // required + interfaceName: 'Meta', // optional fields: [ // required { - name: "title", - type: "text", + name: 'title', + type: 'text', required: true, minLength: 20, maxLength: 100, }, { - name: "description", - type: "textarea", + name: 'description', + type: 'textarea', required: true, minLength: 40, maxLength: 160, @@ -79,5 +79,5 @@ export const ExampleCollection: CollectionConfig = { ], }, ], -}; +} ``` diff --git a/docs/fields/json.mdx b/docs/fields/json.mdx index 34c8debc2..0a17fdda8 100644 --- a/docs/fields/json.mdx +++ b/docs/fields/json.mdx @@ -7,37 +7,38 @@ desc: The JSON field type will store any string in the Database. Learn how to us keywords: json, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express --- - - The JSON field type saves actual JSON in the database, which differs from the Code field that saves the value as a string in the database. + + The JSON field type saves actual JSON in the database, which differs from the Code field that + saves the value as a string in the database. - This field uses the `monaco-react` editor syntax highlighting. ### Config -| Option | Description | -| ---------------- | ----------- | -| **`name`** * | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | -| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. | -| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | -| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | -| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | -| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | -| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | -| **`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) | -| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | -| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) | -| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | -| **`required`** | Require this field to have a value. | -| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | -| **`custom`** | Extension point for adding custom data (e.g. for plugins) | +| Option | Description | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | +| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. | +| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | +| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | +| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | +| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | +| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | +| **`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) | +| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | +| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) | +| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | +| **`required`** | Require this field to have a value. | +| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | +| **`custom`** | Extension point for adding custom data (e.g. for plugins) | _\* An asterisk denotes that a property is required._ @@ -45,15 +46,16 @@ _\* An asterisk denotes that a property is required._ In addition to the default [field admin config](/docs/fields/overview#admin-config), you can adjust the following properties: -| Option | Description | -| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`editorOptions`** | Options that can be passed to the monaco editor, [view the full list](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IDiffEditorConstructionOptions.html). | +| Option | Description | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`editorOptions`** | Options that can be passed to the monaco editor, [view the full list](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IDiffEditorConstructionOptions.html). | ### Example `collections/ExampleCollection.ts + ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', @@ -62,7 +64,7 @@ export const ExampleCollection: CollectionConfig = { name: 'customerJSON', // required type: 'json', // required required: true, - } - ] -}; + }, + ], +} ``` diff --git a/docs/fields/number.mdx b/docs/fields/number.mdx index b78eb4d60..6b6786342 100644 --- a/docs/fields/number.mdx +++ b/docs/fields/number.mdx @@ -6,42 +6,43 @@ desc: Number fields store and validate numeric data. Learn how to use and format keywords: number, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express --- - - The Number field stores and validates numeric entry and supports additional numerical validation and formatting features. + + The Number field stores and validates numeric entry and supports additional numerical validation + and formatting features. - ### Config -| Option | Description | -| ---------------- | ----------- | -| **`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. | -| **`min`** | Minimum value accepted. Used in the default `validation` function. | -| **`max`** | Maximum value accepted. Used in the default `validation` function. | -| **`hasMany`** | Makes this field an ordered array of numbers instead of just a single number. | -| **`minRows`** | Minimum number of numbers in the numbers array, if `hasMany` is set to true. | -| **`maxRows`** | Maximum number of numbers in the numbers array, if `hasMany` is set to true. | -| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | -| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | -| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | -| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | -| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | -| **`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) | -| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | -| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) | -| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | -| **`required`** | Require this field to have a value. | -| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | -| **`custom`** | Extension point for adding custom data (e.g. for plugins) | +| Option | Description | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`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. | +| **`min`** | Minimum value accepted. Used in the default `validation` function. | +| **`max`** | Maximum value accepted. Used in the default `validation` function. | +| **`hasMany`** | Makes this field an ordered array of numbers instead of just a single number. | +| **`minRows`** | Minimum number of numbers in the numbers array, if `hasMany` is set to true. | +| **`maxRows`** | Maximum number of numbers in the numbers array, if `hasMany` is set to true. | +| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | +| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | +| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | +| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | +| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | +| **`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) | +| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | +| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) | +| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | +| **`required`** | Require this field to have a value. | +| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | +| **`custom`** | Extension point for adding custom data (e.g. for plugins) | -*\* An asterisk denotes that a property is required.* +_\* An asterisk denotes that a property is required._ ### Admin config @@ -62,8 +63,9 @@ Set this property to a string that will be used for browser autocomplete. ### Example `collections/ExampleCollection.ts` + ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', @@ -74,8 +76,8 @@ export const ExampleCollection: CollectionConfig = { required: true, admin: { step: 1, - } - } - ] -}; + }, + }, + ], +} ``` diff --git a/docs/fields/overview.mdx b/docs/fields/overview.mdx index 29146f8b3..7e3315af8 100644 --- a/docs/fields/overview.mdx +++ b/docs/fields/overview.mdx @@ -7,8 +7,9 @@ keywords: overview, fields, config, configuration, documentation, Content Manage --- - Fields are the building blocks of Payload. Collections and Globals both use Fields to define the shape of the data - that they store. Payload offers a wide variety of field types - both simple and complex. + Fields are the building blocks of Payload. Collections and Globals both use Fields to define the + shape of the data that they store. Payload offers a wide variety of field types - both simple and + complex. Fields are defined as an array on Collections and Globals via the `fields` key. They define the shape of the data that will be stored as well as automatically construct the corresponding Admin UI. @@ -16,22 +17,23 @@ Fields are defined as an array on Collections and Globals via the `fields` key. The required `type` property on a field determines what values it can accept, how it is presented in the API, and how the field will be rendered in the admin interface. **Simple collection with two fields:** + ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const Page: CollectionConfig = { - slug: 'pages', - fields: [ - { - name: 'myField', - type: 'text', // highlight-line - }, - { - name: 'otherField', - type: 'checkbox', // highlight-line - }, - ], -}; + slug: 'pages', + fields: [ + { + name: 'myField', + type: 'text', // highlight-line + }, + { + name: 'otherField', + type: 'checkbox', // highlight-line + }, + ], +} ``` ### Field types @@ -73,11 +75,12 @@ Some fields use their `name` property as a unique identifier to store and retrie Field validation is enforced automatically based on the field type and other properties such as `required` or `min` and `max` value constraints on certain field types. This default behavior can be replaced by providing your own validate function for any field. It will be used on both the frontend and the backend, so it should not rely on any Node-specific packages. The validation function can be either synchronous or asynchronous and expects to return either `true` or a string error message to display in both API responses and within the Admin panel. There are two arguments available to custom validation functions. + 1. The value which is currently assigned to the field 2. An optional object with dynamic properties for more complex validation having the following: | Property | Description | -|---------------|--------------------------------------------------------------------------------------------------------------------------| +| ------------- | ------------------------------------------------------------------------------------------------------------------------ | | `data` | An object of the full collection or global document. | | `siblingData` | An object of the document data limited to fields within the same parent to the field. | | `operation` | Will be "create" or "update" depending on the UI action or API call. | @@ -87,8 +90,9 @@ There are two arguments available to custom validation functions. | `payload` | If the `validate` function is being executed on the server, Payload will be exposed for easily running local operations. | Example: + ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const Orders: CollectionConfig = { slug: 'orders', @@ -99,38 +103,39 @@ export const Orders: CollectionConfig = { validate: async (val, { operation }) => { if (operation !== 'create') { // skip validation on update - return true; + return true } - const response = await fetch(`https://your-api.com/customers/${val}`); + const response = await fetch(`https://your-api.com/customers/${val}`) if (response.ok) { - return true; + return true } - return 'The customer number provided does not match any customers within our records.'; + return 'The customer number provided does not match any customers within our records.' }, }, ], -}; +} ``` When supplying a field `validate` function, Payload will use yours in place of the default. To make use of the default field validation in your custom logic you can import, call and return the result as needed. For example: + ```ts -import { text } from 'payload/fields/validations'; +import { text } from 'payload/fields/validations' const field: Field = { name: 'notBad', type: 'text', validate: (val, args) => { if (val === 'bad') { - return 'This cannot be "bad"'; + return 'This cannot be "bad"' } // highlight-start - return text(val, args); + return text(val, args) // highlight-end }, -}; +} ``` ### Customizable ID @@ -140,6 +145,7 @@ Users are then required to provide a custom ID value when creating a record thro Valid ID types are `number` and `text`. Example: + ```ts { fields: [ @@ -155,19 +161,19 @@ Example: In addition to each field's base configuration, you can define specific traits and properties for fields that only have effect on how they are rendered in the Admin panel. The following properties are available for all fields within the `admin` property: -| Option | Description | -|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `condition` | You can programmatically show / hide fields based on what other fields are doing. [Click here](#conditional-logic) for more info. | -| `components` | All field components can be completely and easily swapped out for custom components that you define. [Click here](#custom-components) for more info. | -| `description` | Helper text to display with the field to provide more information for the editor user. [Click here](#description) for more info. | -| `position` | Specify if the field should be rendered in the sidebar by defining `position: 'sidebar'`. | +| Option | Description | +| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `condition` | You can programmatically show / hide fields based on what other fields are doing. [Click here](#conditional-logic) for more info. | +| `components` | All field components can be completely and easily swapped out for custom components that you define. [Click here](#custom-components) for more info. | +| `description` | Helper text to display with the field to provide more information for the editor user. [Click here](#description) for more info. | +| `position` | Specify if the field should be rendered in the sidebar by defining `position: 'sidebar'`. | | `width` | Restrict the width of a field. you can pass any string-based value here, be it pixels, percentages, etc. This property is especially useful when fields are nested within a `Row` type where they can be organized horizontally. | -| `style` | Attach raw CSS style properties to the root DOM element of a field. | -| `className` | Attach a CSS class name to the root DOM element of a field. | -| `readOnly` | Setting a field to `readOnly` has no effect on the API whatsoever but disables the admin component's editability to prevent editors from modifying the field's value. | -| `disabled` | If a field is `disabled`, it is completely omitted from the Admin panel. | -| `disableBulkEdit` | Set `disableBulkEdit` to `true` to prevent fields from appearing in the select options when making edits for multiple documents. | -| `hidden` | Setting a field's `hidden` property on its `admin` config will transform it into a `hidden` input type. Its value will still submit with the Admin panel's requests, but the field itself will not be visible to editors. | +| `style` | Attach raw CSS style properties to the root DOM element of a field. | +| `className` | Attach a CSS class name to the root DOM element of a field. | +| `readOnly` | Setting a field to `readOnly` has no effect on the API whatsoever but disables the admin component's editability to prevent editors from modifying the field's value. | +| `disabled` | If a field is `disabled`, it is completely omitted from the Admin panel. | +| `disableBulkEdit` | Set `disableBulkEdit` to `true` to prevent fields from appearing in the select options when making edits for multiple documents. | +| `hidden` | Setting a field's `hidden` property on its `admin` config will transform it into a `hidden` input type. Its value will still submit with the Admin panel's requests, but the field itself will not be visible to editors. | ### Custom components @@ -200,14 +206,14 @@ The `condition` function should return a boolean that will control if the field // highlight-start condition: (data, siblingData, { user }) => { if (data.enableGreeting) { - return true; + return true } else { - return false; + return false } - } + }, // highlight-end - } - } + }, + }, ] } ``` @@ -225,17 +231,17 @@ Here is an example of a defaultValue function that uses both: ```ts const translation: { - en: 'Written by', - es: 'Escrito por', -}; + en: 'Written by' + es: 'Escrito por' +} const field = { name: 'attribution', type: 'text', // highlight-start - defaultValue: ({ user, locale }) => (`${translation[locale]} ${user.name}`) + defaultValue: ({ user, locale }) => `${translation[locale]} ${user.name}`, // highlight-end -}; +} ``` @@ -245,6 +251,7 @@ const field = { ### Description A description can be configured three ways. + - As a string - As a function that accepts an object containing the field's value, which returns a string - As a React component that accepts value as a prop @@ -262,15 +269,17 @@ As shown above, you can simply provide a string that will show by the field, but maxLength: 20, admin: { description: ({ value }) => - (`${typeof value === 'string' ? 20 - value.length : '20'} characters left`) - } - } + `${typeof value === 'string' ? 20 - value.length : '20'} characters left`, + }, + }, ] } ``` + This example will display the number of characters allowed as the user types. **Component Example:** + ```ts { fields: [ @@ -292,6 +301,7 @@ This example will display the number of characters allowed as the user types. ] } ``` + This component will count the number of characters entered. ### TypeScript @@ -299,7 +309,5 @@ This component will count the number of characters entered. You can import the internal Payload `Field` type as well as other common field types as follows: ```ts -import type { - Field, -} from 'payload/types'; +import type { Field } from 'payload/types' ``` diff --git a/docs/fields/point.mdx b/docs/fields/point.mdx index 2494ee14d..7d39f982c 100644 --- a/docs/fields/point.mdx +++ b/docs/fields/point.mdx @@ -7,45 +7,47 @@ desc: The Point field type stores coordinates in the database. Learn how to use keywords: point, geolocation, geospatial, geojson, 2dsphere, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express --- - - The Point field type saves a pair of coordinates in the database and assigns an index for location related queries. + + The Point field type saves a pair of coordinates in the database and assigns an index for location + related queries. - The data structure in the database matches the GeoJSON structure to represent point. The Payload APIs simplifies the object data to only the [longitude, latitude] location. ### Config -| Option | Description | -| ---------------- | ----------- | -| **`name`** * | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | -| **`label`** | Used as a field label in the Admin panel and to name the generated GraphQL type. | -| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | -| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. To support location queries, point index defaults to `2dsphere`, to disable the index set to `false`. | -| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | -| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | -| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | -| **`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) | -| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | -| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) | -| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | -| **`required`** | Require this field to have a value. | -| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | -| **`custom`** | Extension point for adding custom data (e.g. for plugins) | +| Option | Description | +| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | +| **`label`** | Used as a field label in the Admin panel and to name the generated GraphQL type. | +| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | +| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. To support location queries, point index defaults to `2dsphere`, to disable the index set to `false`. | +| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | +| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | +| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | +| **`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) | +| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | +| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) | +| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | +| **`required`** | Require this field to have a value. | +| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | +| **`custom`** | Extension point for adding custom data (e.g. for plugins) | -*\* An asterisk denotes that a property is required.* +_\* An asterisk denotes that a property is required._ ### Example `collections/ExampleCollection.ts` + ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', @@ -55,8 +57,8 @@ export const ExampleCollection: CollectionConfig = { type: 'point', label: 'Location', }, - ] -}; + ], +} ``` ### Querying diff --git a/docs/fields/radio.mdx b/docs/fields/radio.mdx index 46f93561e..29e520c8c 100644 --- a/docs/fields/radio.mdx +++ b/docs/fields/radio.mdx @@ -6,41 +6,46 @@ desc: The Radio field type allows for the selection of one value from a predefin keywords: radio, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express --- - - The Radio Group field type allows for the selection of one value from a predefined set of possible values and presents a radio group-style set of inputs to the Admin panel. + + The Radio Group field type allows for the selection of one value from a predefined set of possible + values and presents a radio group-style set of inputs to the Admin panel. - ### Config -| Option | Description | -| ---------------- | ----------- | -| **`name`** * | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | -| **`options`** * | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing an `label` string and a `value` string. | -| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. | -| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | -| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | -| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | -| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | -| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) | -| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | -| **`defaultValue`** | Provide data to be used for this field's default value. The default value must exist within provided values in `options`. [More](/docs/fields/overview#default-values) | -| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | -| **`required`** | Require this field to have a value. | -| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | -| **`custom`** | Extension point for adding custom data (e.g. for plugins) | +| Option | Description | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | +| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing an `label` string and a `value` string. | +| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. | +| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | +| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | +| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | +| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | +| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) | +| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | +| **`defaultValue`** | Provide data to be used for this field's default value. The default value must exist within provided values in `options`. [More](/docs/fields/overview#default-values) | +| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | +| **`required`** | Require this field to have a value. | +| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | +| **`custom`** | Extension point for adding custom data (e.g. for plugins) | -*\* An asterisk denotes that a property is required.* +_\* An asterisk denotes that a property is required._ - Important:
- Option values should be strings that do not contain hyphens or special characters due to GraphQL enumeration naming constraints. Underscores are allowed. If you determine you need your option values to be non-strings or contain special characters, they will be formatted accordingly before being used as a GraphQL enum. + Important: +
+ Option values should be strings that do not contain hyphens or special characters due to GraphQL + enumeration naming constraints. Underscores are allowed. If you determine you need your option + values to be non-strings or contain special characters, they will be formatted accordingly before + being used as a GraphQL enum.
### Admin config @@ -54,8 +59,9 @@ The `layout` property allows for the radio group to be styled as a horizonally o ### Example `collections/ExampleCollection.ts` + ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', @@ -63,7 +69,8 @@ export const ExampleCollection: CollectionConfig = { { name: 'color', // required type: 'radio', // required - options: [ // required + options: [ + // required { label: 'Mint', value: 'mint', @@ -76,8 +83,8 @@ export const ExampleCollection: CollectionConfig = { defaultValue: 'mint', // The first value in options. admin: { layout: 'horizontal', - } - } - ] + }, + }, + ], } ``` diff --git a/docs/fields/relationship.mdx b/docs/fields/relationship.mdx index ae13e95f9..864ade0b1 100644 --- a/docs/fields/relationship.mdx +++ b/docs/fields/relationship.mdx @@ -7,15 +7,15 @@ keywords: relationship, fields, config, configuration, documentation, Content Ma --- - The Relationship field is one of the most powerful fields Payload features. It - provides for the ability to easily relate documents together. + The Relationship field is one of the most powerful fields Payload features. It provides for the + ability to easily relate documents together. - **Example uses:** @@ -54,8 +54,8 @@ _\* An asterisk denotes that a property is required._ Tip:
- The [Depth](/docs/getting-started/concepts#depth) parameter can be used to - automatically populate related documents that are returned by the API. + The [Depth](/docs/getting-started/concepts#depth) parameter can be used to automatically populate + related documents that are returned by the API.
### Admin config @@ -87,32 +87,32 @@ The `filterOptions` property can either be a `Where` query directly, or a functi ### Example ```ts -import { CollectionConfig } from "payload/types"; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { - slug: "example-collection", + slug: 'example-collection', fields: [ { - name: "purchase", - type: "relationship", - relationTo: ["products", "services"], + name: 'purchase', + type: 'relationship', + relationTo: ['products', 'services'], filterOptions: ({ relationTo, siblingData }) => { // returns a Where query dynamically by the type of relationship - if (relationTo === "products") { + if (relationTo === 'products') { return { stock: { greater_than: siblingData.quantity }, - }; + } } - if (relationTo === "services") { + if (relationTo === 'services') { return { isAvailable: { equals: true }, - }; + } } }, }, ], -}; +} ``` You can learn more about writing queries [here](/docs/queries/overview). @@ -120,11 +120,10 @@ You can learn more about writing queries [here](/docs/queries/overview). Note:
- When a relationship field has both filterOptions and a custom{" "} - validate function, the api will not validate{" "} - filterOptions unless you call the default relationship field - validation function imported from payload/fields/validations{" "} - in your validate function. + When a relationship field has both filterOptions and a custom{' '} + validate function, the api will not validate filterOptions{' '} + unless you call the default relationship field validation function imported from{' '} + payload/fields/validations in your validate function.
### How the data is saved diff --git a/docs/fields/rich-text.mdx b/docs/fields/rich-text.mdx index a23c38321..7501665e9 100644 --- a/docs/fields/rich-text.mdx +++ b/docs/fields/rich-text.mdx @@ -7,20 +7,27 @@ keywords: rich text, fields, config, configuration, documentation, Content Manag --- - The Rich Text field is a powerful way to allow editors to write dynamic content. The content is saved as JSON in the database and can be converted into any format, including HTML, that you need. + The Rich Text field is a powerful way to allow editors to write dynamic content. The content is + saved as JSON in the database and can be converted into any format, including HTML, that you need. - The Admin component is built on the powerful [`slatejs`](https://docs.slatejs.org/) editor and is meant to be as extensible and customizable as possible. - Consistent with Payload's goal of making you learn as little of Payload as possible, customizing and using the Rich Text Editor does not involve learning how to develop for a Payload rich text editor. Instead, you can invest your time and effort into learning Slate, an open-source tool that will allow you to apply your learnings elsewhere as well. + + Consistent with Payload's goal of making you learn as little of Payload as possible, customizing + and using the Rich Text Editor does not involve learning how to develop for a Payload{' '} + rich text editor. + {' '} + Instead, you can invest your time and effort into learning Slate, an open-source tool that will + allow you to apply your learnings elsewhere as well. ### Config @@ -120,7 +127,12 @@ The built-in `relationship` element is a powerful way to reference other Documen Similar to the `relationship` element, the `upload` element is a user-friendly way to reference [Upload-enabled collections](/docs/upload/overview) with a UI specifically designed for media / image-based uploads. - Tip:
Collections are automatically allowed to be selected within the Rich Text relationship and upload elements by default. If you want to disable a collection from being able to be referenced in Rich Text fields, set the collection admin options of enableRichTextLink and enableRichTextRelationship to false. + Tip: +
+ Collections are automatically allowed to be selected within the Rich Text relationship and upload + elements by default. If you want to disable a collection from being able to be referenced in Rich + Text fields, set the collection admin options of enableRichTextLink and{' '} + enableRichTextRelationship to false.
Relationship and Upload elements are populated dynamically into your Rich Text field' content. Within the REST and Local APIs, any present RichText `relationship` or `upload` elements will respect the `depth` option that you pass, and will be populated accordingly. In GraphQL, each `richText` field accepts an argument of `depth` for you to utilize. @@ -159,29 +171,29 @@ Specifying custom `Type`s let you extend your custom elements by adding addition `collections/ExampleCollection.ts` ```ts -import { CollectionConfig } from "payload/types"; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { - slug: "example-collection", + slug: 'example-collection', fields: [ { - name: "content", // required - type: "richText", // required + name: 'content', // required + type: 'richText', // required defaultValue: [ { - children: [{ text: "Here is some default content for this field" }], + children: [{ text: 'Here is some default content for this field' }], }, ], required: true, admin: { elements: [ - "h2", - "h3", - "h4", - "link", - "blockquote", + 'h2', + 'h3', + 'h4', + 'link', + 'blockquote', { - name: "cta", + name: 'cta', Button: CustomCallToActionButton, Element: CustomCallToActionElement, plugins: [ @@ -190,10 +202,10 @@ export const ExampleCollection: CollectionConfig = { }, ], leaves: [ - "bold", - "italic", + 'bold', + 'italic', { - name: "highlight", + name: 'highlight', Button: CustomHighlightButton, Leaf: CustomHighlightLeaf, plugins: [ @@ -205,11 +217,11 @@ export const ExampleCollection: CollectionConfig = { // Inject your own fields into the Link element fields: [ { - name: "rel", - label: "Rel Attribute", - type: "select", + name: 'rel', + label: 'Rel Attribute', + type: 'select', hasMany: true, - options: ["noopener", "noreferrer", "nofollow"], + options: ['noopener', 'noreferrer', 'nofollow'], }, ], }, @@ -226,7 +238,7 @@ export const ExampleCollection: CollectionConfig = { }, }, ], -}; +} ``` For more examples regarding how to define your own elements and leaves, check out the example [`RichText` field](https://github.com/payloadcms/public-demo/blob/master/src/fields/hero.ts) within the Public Demo source code. @@ -296,7 +308,10 @@ const serialize = (children) => ``` - Note:
The above example is for how to render to JSX, although for plain HTML the pattern is similar. Just remove the JSX and return HTML strings instead! + Note: +
+ The above example is for how to render to JSX, although for plain HTML the pattern is similar. + Just remove the JSX and return HTML strings instead!
### Built-in SlateJS Plugins @@ -312,26 +327,26 @@ If you want to utilize this functionality within your own custom elements, you c `customLargeBodyElement.js`: ```ts -import Button from "./Button"; -import Element from "./Element"; -import withLargeBody from "./plugin"; +import Button from './Button' +import Element from './Element' +import withLargeBody from './plugin' export default { - name: "large-body", + name: 'large-body', Button, Element, plugins: [ (incomingEditor) => { - const editor = incomingEditor; - const { shouldBreakOutOnEnter } = editor; + const editor = incomingEditor + const { shouldBreakOutOnEnter } = editor editor.shouldBreakOutOnEnter = (element) => - element.type === "large-body" ? true : shouldBreakOutOnEnter(element); + element.type === 'large-body' ? true : shouldBreakOutOnEnter(element) - return editor; + return editor }, ], -}; +} ``` Above, you can see that we are creating a custom SlateJS element with a name of `large-body`. This might render a slightly larger body copy on the frontend of your app(s). We pass it a name, button, and element—but additionally, we pass it a `plugins` array containing a single SlateJS plugin. @@ -343,5 +358,5 @@ The plugin itself extends Payload's built-in `shouldBreakOutOnEnter` Slate funct If you are building your own custom Rich Text elements or leaves, you may benefit from importing the following types: ```ts -import type { RichTextCustomElement, RichTextCustomLeaf } from "payload/types"; +import type { RichTextCustomElement, RichTextCustomLeaf } from 'payload/types' ``` diff --git a/docs/fields/row.mdx b/docs/fields/row.mdx index b6cfee864..9b1514135 100644 --- a/docs/fields/row.mdx +++ b/docs/fields/row.mdx @@ -6,39 +6,42 @@ desc: With the Row field you can arrange fields next to each other in the Admin keywords: row, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express --- - - The Row field is presentational-only and only affects the Admin panel. By using it, you can arrange fields next to each other horizontally. + + The Row field is presentational-only and only affects the Admin panel. By using it, you can + arrange fields next to each other horizontally. - ### Config -| Option | Description | -| ---------------- | ----------- | -| **`fields`** * | Array of field types to nest within this Row. | -| **`admin`** | Admin-specific configuration excluding `description`, `readOnly`, and `hidden`. See the [default field admin config](/docs/fields/overview#admin-config) for more details. | -| **`custom`** | Extension point for adding custom data (e.g. for plugins) | +| Option | Description | +| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`fields`** \* | Array of field types to nest within this Row. | +| **`admin`** | Admin-specific configuration excluding `description`, `readOnly`, and `hidden`. See the [default field admin config](/docs/fields/overview#admin-config) for more details. | +| **`custom`** | Extension point for adding custom data (e.g. for plugins) | -*\* An asterisk denotes that a property is required.* +_\* An asterisk denotes that a property is required._ ### Example `collections/ExampleCollection.ts` + ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { type: 'row', // required - fields: [ // required + fields: [ + // required { name: 'label', type: 'text', @@ -56,8 +59,7 @@ export const ExampleCollection: CollectionConfig = { }, }, ], - } - ] + }, + ], } - ``` diff --git a/docs/fields/select.mdx b/docs/fields/select.mdx index 4c87ae88f..0868db554 100644 --- a/docs/fields/select.mdx +++ b/docs/fields/select.mdx @@ -7,21 +7,21 @@ keywords: select, multi-select, fields, config, configuration, documentation, Co --- - The Select field provides a dropdown-style interface for choosing options from - a predefined list as an enumeration. + The Select field provides a dropdown-style interface for choosing options from a predefined list + as an enumeration. - ### Config | Option | Description | -| ------------------ |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | | **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. | | **`hasMany`** | Boolean when, if set to `true`, allows this field to have many selections instead of only one. | @@ -44,11 +44,10 @@ _\* An asterisk denotes that a property is required._ Important:
- Option values should be strings that do not contain hyphens or special - characters due to GraphQL enumeration naming constraints. Underscores are - allowed. If you determine you need your option values to be non-strings or - contain special characters, they will be formatted accordingly before being - used as a GraphQL enum. + Option values should be strings that do not contain hyphens or special characters due to GraphQL + enumeration naming constraints. Underscores are allowed. If you determine you need your option + values to be non-strings or contain special characters, they will be formatted accordingly before + being used as a GraphQL enum.
### Admin config @@ -66,8 +65,9 @@ Set to `true` if you'd like this field to be sortable within the Admin UI using ### Example `collections/ExampleCollection.ts` + ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', @@ -94,8 +94,7 @@ export const ExampleCollection: CollectionConfig = { value: 'carbon_fiber_dashboard', }, ], - } - ] + }, + ], } - ``` diff --git a/docs/fields/tabs.mdx b/docs/fields/tabs.mdx index afc7cb8d2..7a62a0c4f 100644 --- a/docs/fields/tabs.mdx +++ b/docs/fields/tabs.mdx @@ -7,19 +7,18 @@ keywords: tabs, fields, config, configuration, documentation, Content Management --- - The Tabs field is presentational-only and only affects the Admin panel (unless - a tab is named). By using it, you can place fields within a nice layout - component that separates certain sub-fields by a tabbed interface. + The Tabs field is presentational-only and only affects the Admin panel (unless a tab is named). By + using it, you can place fields within a nice layout component that separates certain sub-fields by + a tabbed interface. - - ### Config | Option | Description | @@ -32,12 +31,12 @@ keywords: tabs, fields, config, configuration, documentation, Content Management Each tab must have either a `name` or `label` and the required `fields` array. You can also optionally pass a `description` to render within each individual tab. -| Option | Description | -| ---------------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **`name`** | Groups field data into an object when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | -| **`label`** | The label to render on the tab itself. Required when name is undefined, defaults to name converted to words. | -| **`fields`** \* | The fields to render within this tab. | -| **`description`** | Optionally render a description within this tab to describe the contents of the tab itself. | +| Option | Description | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **`name`** | Groups field data into an object when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | +| **`label`** | The label to render on the tab itself. Required when name is undefined, defaults to name converted to words. | +| **`fields`** \* | The fields to render within this tab. | +| **`description`** | Optionally render a description within this tab to describe the contents of the tab itself. | | **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). (`name` must be present) | _\* An asterisk denotes that a property is required._ @@ -47,36 +46,36 @@ _\* An asterisk denotes that a property is required._ `collections/ExampleCollection.ts` ```ts -import { CollectionConfig } from "payload/types"; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { - slug: "example-collection", + slug: 'example-collection', fields: [ { - type: "tabs", // required + type: 'tabs', // required tabs: [ // required { - label: "Tab One Label", // required - description: "This will appear within the tab above the fields.", + label: 'Tab One Label', // required + description: 'This will appear within the tab above the fields.', fields: [ // required { - name: "someTextField", - type: "text", + name: 'someTextField', + type: 'text', required: true, }, ], }, { - name: "tabTwo", - label: "Tab Two Label", // required - interfaceName: "TabTwo", // optional (`name` must be present) + name: 'tabTwo', + label: 'Tab Two Label', // required + interfaceName: 'TabTwo', // optional (`name` must be present) fields: [ // required { - name: "numberField", // accessible via tabTwo.numberField - type: "number", + name: 'numberField', // accessible via tabTwo.numberField + type: 'number', required: true, }, ], @@ -84,5 +83,5 @@ export const ExampleCollection: CollectionConfig = { ], }, ], -}; +} ``` diff --git a/docs/fields/text.mdx b/docs/fields/text.mdx index 5df281b8e..9c27159e6 100644 --- a/docs/fields/text.mdx +++ b/docs/fields/text.mdx @@ -6,39 +6,40 @@ desc: Text field types simply save a string to the database and provide the Admi keywords: text, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express --- - - The Text field type is one of the most commonly used fields. It saves a string to the database and provides the Admin panel with a simple text input. + + The Text field type is one of the most commonly used fields. It saves a string to the database and + provides the Admin panel with a simple text input. - ### Config -| Option | Description | -| ---------------- | ----------- | -| **`name`** * | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | -| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. | -| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | -| **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. | -| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. | -| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | -| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | -| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | -| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | -| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) | -| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | -| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) | -| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | -| **`required`** | Require this field to have a value. | -| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | -| **`custom`** | Extension point for adding custom data (e.g. for plugins) | +| Option | Description | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | +| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. | +| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | +| **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. | +| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. | +| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | +| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | +| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | +| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | +| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) | +| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | +| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) | +| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | +| **`required`** | Require this field to have a value. | +| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | +| **`custom`** | Extension point for adding custom data (e.g. for plugins) | -*\* An asterisk denotes that a property is required.* +_\* An asterisk denotes that a property is required._ ### Admin config @@ -59,8 +60,9 @@ Override the default text direction of the Admin panel for this field. Set to `t ### Example `collections/ExampleCollection.ts` + ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', @@ -69,8 +71,7 @@ export const ExampleCollection: CollectionConfig = { name: 'pageTitle', // required type: 'text', // required required: true, - } - ] + }, + ], } - ``` diff --git a/docs/fields/textarea.mdx b/docs/fields/textarea.mdx index 9dfd367ea..96a9b8258 100644 --- a/docs/fields/textarea.mdx +++ b/docs/fields/textarea.mdx @@ -6,39 +6,40 @@ desc: Textarea field types save a string to the database, similar to the Text fi keywords: textarea, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express --- - - The Textarea field is almost identical to the Text field but it features a slightly larger input that is better suited to edit longer text. + + The Textarea field is almost identical to the Text field but it features a slightly larger input + that is better suited to edit longer text. - ### Config -| Option | Description | -| ---------------- | ----------- | -| **`name`** * | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | -| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. | -| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | -| **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. | -| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. | -| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | -| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | -| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | -| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | -| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) | -| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | -| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values)| -| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | -| **`required`** | Require this field to have a value. | -| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | -| **`custom`** | Extension point for adding custom data (e.g. for plugins) | +| Option | Description | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | +| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. | +| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. | +| **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. | +| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. | +| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) | +| **`index`** | Build a [MongoDB index](https://docs.mongodb.com/manual/indexes/) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | +| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. | +| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | +| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) | +| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. | +| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) | +| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. | +| **`required`** | Require this field to have a value. | +| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | +| **`custom`** | Extension point for adding custom data (e.g. for plugins) | -*\* An asterisk denotes that a property is required.* +_\* An asterisk denotes that a property is required._ ### Admin config @@ -59,8 +60,9 @@ Override the default text direction of the Admin panel for this field. Set to `t ### Example `collections/ExampleCollection.ts` + ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', @@ -69,8 +71,7 @@ export const ExampleCollection: CollectionConfig = { name: 'metaDescription', // required type: 'textarea', // required required: true, - } - ] + }, + ], } - ``` diff --git a/docs/fields/ui.mdx b/docs/fields/ui.mdx index 2a10c878c..6609c792f 100644 --- a/docs/fields/ui.mdx +++ b/docs/fields/ui.mdx @@ -7,10 +7,9 @@ keywords: custom field, react component, fields, config, configuration, document --- - The UI (user interface) field gives you a ton of power to add your own React - components directly into the Admin panel, nested directly within your other - fields. It has absolutely no effect on the data of your documents. It is - presentational-only. + The UI (user interface) field gives you a ton of power to add your own React components directly + into the Admin panel, nested directly within your other fields. It has absolutely no effect on the + data of your documents. It is presentational-only. This field is helpful if you need to build in custom functionality via React components within the Admin panel. Think of it as a way for you to "plug in" your own React components directly within your other fields, so you can provide your editors with new controls exactly where you want them to go. @@ -41,14 +40,14 @@ _\* An asterisk denotes that a property is required._ `collections/ExampleCollection.ts` ```ts -import { CollectionConfig } from "payload/types"; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { - slug: "example-collection", + slug: 'example-collection', fields: [ { - name: "myCustomUIField", // required - type: "ui", // required + name: 'myCustomUIField', // required + type: 'ui', // required admin: { components: { Field: MyCustomUIField, @@ -57,5 +56,5 @@ export const ExampleCollection: CollectionConfig = { }, }, ], -}; +} ``` diff --git a/docs/fields/upload.mdx b/docs/fields/upload.mdx index 5fe888a3e..17458f9a6 100644 --- a/docs/fields/upload.mdx +++ b/docs/fields/upload.mdx @@ -7,19 +7,23 @@ keywords: upload, images media, fields, config, configuration, documentation, Co --- - The Upload field allows for the selection of a Document from a collection supporting Uploads, and formats the selection as a thumbnail in the Admin panel. + The Upload field allows for the selection of a Document from a collection supporting Uploads, and + formats the selection as a thumbnail in the Admin panel. - Important:
- To use this field, you need to have a Collection configured to allow Uploads. For more information, [click here](/docs/upload/overview) to read about how to enable Uploads on a collection by collection basis. + Important: +
+ To use this field, you need to have a Collection configured to allow Uploads. For more + information, [click here](/docs/upload/overview) to read about how to enable Uploads on a + collection by collection basis.
- **Example uses:** @@ -57,19 +61,19 @@ _\* An asterisk denotes that a property is required._ `collections/ExampleCollection.ts` ```ts -import { CollectionConfig } from "payload/types"; +import { CollectionConfig } from 'payload/types' export const ExampleCollection: CollectionConfig = { - slug: "example-collection", + slug: 'example-collection', fields: [ { - name: "backgroundImage", // required - type: "upload", // required - relationTo: "media", // required + name: 'backgroundImage', // required + type: 'upload', // required + relationTo: 'media', // required required: true, }, ], -}; +} ``` ### Filtering upload options @@ -90,18 +94,22 @@ The `filterOptions` property can either be a `Where` query directly, or a functi ```ts const uploadField = { - name: "image", - type: "upload", - relationTo: "media", + name: 'image', + type: 'upload', + relationTo: 'media', filterOptions: { - mimeType: { contains: "image" }, + mimeType: { contains: 'image' }, }, -}; +} ``` You can learn more about writing queries [here](/docs/queries/overview). - Note:
- When an upload field has both filterOptions and a custom validate function, the api will not validate filterOptions unless you call the default upload field validation function imported from payload/fields/validations in your validate function. + Note: +
+ When an upload field has both filterOptions and a custom{' '} + validate function, the api will not validate filterOptions{' '} + unless you call the default upload field validation function imported from{' '} + payload/fields/validations in your validate function.
diff --git a/docs/getting-started/concepts.mdx b/docs/getting-started/concepts.mdx index ef88409bc..b87bee24e 100644 --- a/docs/getting-started/concepts.mdx +++ b/docs/getting-started/concepts.mdx @@ -10,16 +10,14 @@ Payload is based around a small and intuitive set of concepts. Before starting t ### Config - - The Payload config is where you configure everything that Payload does. - +The Payload config is where you configure everything that Payload does. By default, the Payload config lives in the root folder of your code and is named `payload.config.js` (`payload.config.ts` if you're using TypeScript), but you can customize its name and where you store it. You can write full functions and even full React components right into your config. ### Collections - A Collection represents a type of content that Payload will store and can contain many documents. + A Collection represents a type of content that Payload will store and can contain many documents. Collections define the shape of your data as well as all functionalities attached to that data. They will contain one or many "documents", all corresponding with the same fields and functionalities that you define. @@ -29,7 +27,8 @@ They can represent anything you can store in a database - for example - pages, p ### Globals - A Global is a "one-off" piece of content that is perfect for storing navigational structures, themes, top-level meta data, and more. + A Global is a "one-off" piece of content that is perfect for storing navigational structures, + themes, top-level meta data, and more. Globals are in many ways similar to Collections, but there is only ever **one** instance of a Global, whereas Collections can contain many documents. @@ -37,7 +36,8 @@ Globals are in many ways similar to Collections, but there is only ever **one** ### Fields - Fields are the building blocks of Payload. Collections and Globals both use Fields to define the shape of the data that they store. + Fields are the building blocks of Payload. Collections and Globals both use Fields to define the + shape of the data that they store. Payload comes with [many different field types](../fields/overview) that give you a ton of flexibility while designing your API. Each Field type has its own potential properties that allow you to customize how they work. @@ -45,7 +45,8 @@ Payload comes with [many different field types](../fields/overview) that give yo ### Hooks - Hooks are where you can "tie in" to existing Payload actions to perform your own additional logic or modify how Payload operates altogether. + Hooks are where you can "tie in" to existing Payload actions to perform your own additional logic + or modify how Payload operates altogether. Hooks are an extremely powerful concept and are central to extending and customizing your app. Payload provides a wide variety of hooks which you can utilize. For example, imagine if you'd like to send an email every time a document is created in your Orders collection. To do so, you can add an `afterChange` hook function to your Orders collection that receives the Order data and allows you to send an email accordingly. @@ -65,10 +66,11 @@ For more, visit the [Access Control documentation](/docs/access-control/overview ### Depth - "Depth" gives you control over how many levels down related documents should be automatically populated when retrieved. + "Depth" gives you control over how many levels down related documents should be automatically + populated when retrieved. -You can specify population `depth` via query parameter in the REST API and by an option in the local API. *Depth has no effect in the GraphQL API, because there, depth is based on the shape of your queries.* +You can specify population `depth` via query parameter in the REST API and by an option in the local API. _Depth has no effect in the GraphQL API, because there, depth is based on the shape of your queries._ It is also possible to limit the depth for specific `relation` and `upload` fields using the `maxDepth` property in your configuration. **For example, let's look at the following Collections:** `departments`, `users`, `posts` @@ -155,6 +157,8 @@ To populate `user.author.department` in it's entirety you could specify `?depth= ``` - Note:
- When access control on collections prevents relationship fields from populating, the API response will contain the relationship id instead of the full document. + Note: +
+ When access control on collections prevents relationship fields from populating, the API response + will contain the relationship id instead of the full document.
diff --git a/docs/getting-started/installation.mdx b/docs/getting-started/installation.mdx index fe25be642..69d93a2d7 100644 --- a/docs/getting-started/installation.mdx +++ b/docs/getting-started/installation.mdx @@ -15,8 +15,7 @@ Payload requires the following software: - A MongoDB Database - Before proceeding any further, please ensure that you have the above - requirements met. + Before proceeding any further, please ensure that you have the above requirements met. ## Quickstart with create-payload-app @@ -36,13 +35,13 @@ Adding Payload to either a new or existing TypeScript + Express app is super str From there, the first step is writing a baseline config. Create a new `payload.config.ts` in your project's `/src` directory (or whatever your root TS dir is). The simplest config contains the following: ```js -import { buildConfig } from "payload/config"; +import { buildConfig } from 'payload/config' export default buildConfig({ // By default, Payload will boot up normally // and you will be provided with a base `User` collection. // But, here is where you define how you'd like Payload to work! -}); +}) ``` Write the above code into your newly created config file. This baseline config will automatically provide you with a default `User` collection. For more information about users and authentication, including how to provide your own user config, jump to the [Authentication](/docs/authentication/config) section. @@ -58,15 +57,13 @@ Now that you've got a baseline Payload config, it's time to initialize Payload. 1. Add the following code to `server.ts`: ```ts -import express from "express"; +import express from 'express' -const app = express(); +const app = express() app.listen(3000, async () => { - console.log( - "Express is now listening for incoming connections on port 3000." - ); -}); + console.log('Express is now listening for incoming connections on port 3000.') +}) ``` This server doesn't do anything just yet. But, after you have this in place, we can initialize Payload via its asynchronous `init()` method, which accepts a small set of arguments to tell it how to operate. @@ -74,27 +71,25 @@ This server doesn't do anything just yet. But, after you have this in place, we To initialize Payload, update your `server.ts` file to reflect the following code: ```ts -import express from "express"; -import payload from "payload"; +import express from 'express' +import payload from 'payload' -require("dotenv").config(); -const app = express(); +require('dotenv').config() +const app = express() const start = async () => { await payload.init({ secret: process.env.PAYLOAD_SECRET, mongoURL: process.env.MONGODB_URI, express: app, - }); + }) app.listen(3000, async () => { - console.log( - "Express is now listening for incoming connections on port 3000." - ); - }); -}; + console.log('Express is now listening for incoming connections on port 3000.') + }) +} -start(); +start() ``` A quick reminder: in this configuration, we're making use of two environmental variables, `process.env.PAYLOAD_SECRET` and `process.env.MONGODB_URI`. Often, it's smart to store these values in an `.env` file at the root of your directory and set different values for each of your environments (local, stage, prod, etc). The `dotenv` package is very handy and works well alongside of Payload. A typical `.env` file will look like this: diff --git a/docs/getting-started/what-is-payload.mdx b/docs/getting-started/what-is-payload.mdx index 3a2f4ea07..9f0d55223 100644 --- a/docs/getting-started/what-is-payload.mdx +++ b/docs/getting-started/what-is-payload.mdx @@ -12,9 +12,8 @@ keywords: documentation, getting started, guide, Content Management System, cms, /> - Payload is a headless CMS and application framework. It’s meant to provide a - massive boost to your development process, but importantly, stay out of your - way as your apps get more complex. + Payload is a headless CMS and application framework. It’s meant to provide a massive boost to your + development process, but importantly, stay out of your way as your apps get more complex. Out of the box, Payload gives you a lot of the things that you often need when developing a new website, web app, or native app: diff --git a/docs/graphql/extending.mdx b/docs/graphql/extending.mdx index 984c613e5..ad8050e98 100644 --- a/docs/graphql/extending.mdx +++ b/docs/graphql/extending.mdx @@ -10,10 +10,10 @@ You can add your own GraphQL queries and mutations to Payload, making use of all To do so, add your queries and mutations to the main Payload config as follows: -| Config Path | Description | -| -------------------- | -------------| -| `graphQL.queries` | Function that returns an object containing keys to custom GraphQL queries | -| `graphQL.mutations` | Function that returns an object containing keys to custom GraphQL mutations | +| Config Path | Description | +| ------------------- | --------------------------------------------------------------------------- | +| `graphQL.queries` | Function that returns an object containing keys to custom GraphQL queries | +| `graphQL.mutations` | Function that returns an object containing keys to custom GraphQL mutations | The above properties each receive a function that is defined with the following arguments: @@ -34,8 +34,8 @@ Both `graphQL.queries` and `graphQL.mutations` functions should return an object `payload.config.js`: ```ts -import { buildConfig } from 'payload/config'; -import myCustomQueryResolver from './graphQL/resolvers/myCustomQueryResolver'; +import { buildConfig } from 'payload/config' +import myCustomQueryResolver from './graphQL/resolvers/myCustomQueryResolver' export default buildConfig({ graphQL: { @@ -57,14 +57,14 @@ export default buildConfig({ args: { argNameHere: { type: new GraphQL.GraphQLNonNull(GraphQLString), - } + }, }, resolve: myCustomQueryResolver, - } + }, } - } + }, // highlight-end - } + }, }) ``` @@ -77,10 +77,10 @@ Your function will receive four arguments you can make use of: Example ```ts -async (obj, args, context, info) => { } +;async (obj, args, context, info) => {} ``` -**`obj`** +**`obj`** The previous object. Not very often used and usually discarded. diff --git a/docs/graphql/overview.mdx b/docs/graphql/overview.mdx index 7028591ba..e6523edf2 100644 --- a/docs/graphql/overview.mdx +++ b/docs/graphql/overview.mdx @@ -102,8 +102,13 @@ GraphQL Playground is enabled by default for development purposes, but disabled You can even log in using the `login[collection-singular-label-here]` mutation to use the Playground as an authenticated user. - Tip:
- To see more regarding how the above queries and mutations are used, visit your GraphQL playground (by default at [http://localhost:3000/api/graphql-playground](http://localhost:3000/api/graphql-playground)) while your server is running. There, you can use the "Schema" and "Docs" buttons on the right to see a ton of detail about how GraphQL operates within Payload. + Tip: +
+ To see more regarding how the above queries and mutations are used, visit your GraphQL playground + (by default at + [http://localhost:3000/api/graphql-playground](http://localhost:3000/api/graphql-playground)) + while your server is running. There, you can use the "Schema" and "Docs" buttons on the right to + see a ton of detail about how GraphQL operates within Payload.
## Query complexity limits diff --git a/docs/hooks/collections.mdx b/docs/hooks/collections.mdx index b998dd9a0..9f3c65f42 100644 --- a/docs/hooks/collections.mdx +++ b/docs/hooks/collections.mdx @@ -70,14 +70,14 @@ The `beforeOperation` hook can be used to modify the arguments that operations a Available Collection operations include `create`, `read`, `update`, `delete`, `login`, `refresh`, and `forgotPassword`. ```ts -import { CollectionBeforeOperationHook } from "payload/types"; +import { CollectionBeforeOperationHook } from 'payload/types' const beforeOperationHook: CollectionBeforeOperationHook = async ({ args, // original arguments passed into the operation operation, // name of the operation }) => { - return args; // return modified operation arguments as necessary -}; + return args // return modified operation arguments as necessary +} ``` ### beforeValidate @@ -91,7 +91,7 @@ Please do note that this does not run before the client-side validation. If you 3. `validate` runs on the server ```ts -import { CollectionBeforeOperationHook } from "payload/types"; +import { CollectionBeforeOperationHook } from 'payload/types' const beforeValidateHook: CollectionBeforeValidateHook = async ({ data, // incoming data to update or create with @@ -99,8 +99,8 @@ const beforeValidateHook: CollectionBeforeValidateHook = async ({ operation, // name of the operation ie. 'create', 'update' originalDoc, // original document }) => { - return data; // Return data to either create or update a document with -}; + return data // Return data to either create or update a document with +} ``` ### beforeChange @@ -108,7 +108,7 @@ const beforeValidateHook: CollectionBeforeValidateHook = async ({ Immediately following validation, `beforeChange` hooks will run within `create` and `update` operations. At this stage, you can be confident that the data that will be saved to the document is valid in accordance to your field validations. You can optionally modify the shape of data to be saved. ```ts -import { CollectionBeforeChangeHook } from "payload/types"; +import { CollectionBeforeChangeHook } from 'payload/types' const beforeChangeHook: CollectionBeforeChangeHook = async ({ data, // incoming data to update or create with @@ -116,8 +116,8 @@ const beforeChangeHook: CollectionBeforeChangeHook = async ({ operation, // name of the operation ie. 'create', 'update' originalDoc, // original document }) => { - return data; // Return data to either create or update a document with -}; + return data // Return data to either create or update a document with +} ``` ### afterChange @@ -125,7 +125,7 @@ const beforeChangeHook: CollectionBeforeChangeHook = async ({ After a document is created or updated, the `afterChange` hook runs. This hook is helpful to recalculate statistics such as total sales within a global, syncing user profile changes to a CRM, and more. ```ts -import { CollectionAfterChangeHook } from "payload/types"; +import { CollectionAfterChangeHook } from 'payload/types' const afterChangeHook: CollectionAfterChangeHook = async ({ doc, // full document data @@ -133,8 +133,8 @@ const afterChangeHook: CollectionAfterChangeHook = async ({ previousDoc, // document data before updating the collection operation, // name of the operation ie. 'create', 'update' }) => { - return doc; -}; + return doc +} ``` ### beforeRead @@ -142,15 +142,15 @@ const afterChangeHook: CollectionAfterChangeHook = async ({ Runs before `find` and `findByID` operations are transformed for output by `afterRead`. This hook fires before hidden fields are removed and before localized fields are flattened into the requested locale. Using this Hook will provide you with all locales and all hidden fields via the `doc` argument. ```ts -import { CollectionBeforeReadHook } from "payload/types"; +import { CollectionBeforeReadHook } from 'payload/types' const beforeReadHook: CollectionBeforeReadHook = async ({ doc, // full document data req, // full express request query, // JSON formatted query }) => { - return doc; -}; + return doc +} ``` ### afterRead @@ -158,7 +158,7 @@ const beforeReadHook: CollectionBeforeReadHook = async ({ Runs as the last step before documents are returned. Flattens locales, hides protected fields, and removes fields that users do not have access to. ```ts -import { CollectionAfterReadHook } from "payload/types"; +import { CollectionAfterReadHook } from 'payload/types' const afterReadHook: CollectionAfterReadHook = async ({ doc, // full document data @@ -166,8 +166,8 @@ const afterReadHook: CollectionAfterReadHook = async ({ query, // JSON formatted query findMany, // boolean to denote if this hook is running against finding one, or finding many }) => { - return doc; -}; + return doc +} ``` ### beforeDelete @@ -204,15 +204,15 @@ The `afterOperation` hook can be used to modify the result of operations or exec Available Collection operations include `create`, `find`, `findByID`, `update`, `updateByID`, `delete`, `deleteByID`, `login`, `refresh`, and `forgotPassword`. ```ts -import { CollectionAfterOperationHook } from "payload/types"; +import { CollectionAfterOperationHook } from 'payload/types' const afterOperationHook: CollectionAfterOperationHook = async ({ args, // arguments passed into the operation operation, // name of the operation result, // the result of the operation, before modifications }) => { - return result; // return modified result as necessary -}; + return result // return modified result as necessary +} ``` ### beforeLogin @@ -220,14 +220,14 @@ const afterOperationHook: CollectionAfterOperationHook = async ({ For auth-enabled Collections, this hook runs during `login` operations where a user with the provided credentials exist, but before a token is generated and added to the response. You can optionally modify the user that is returned, or throw an error in order to deny the login operation. ```ts -import { CollectionBeforeLoginHook } from "payload/types"; +import { CollectionBeforeLoginHook } from 'payload/types' const beforeLoginHook: CollectionBeforeLoginHook = async ({ req, // full express request user, // user being logged in }) => { - return user; -}; + return user +} ``` ### afterLogin @@ -288,15 +288,15 @@ const afterMeHook: CollectionAfterMeHook = async ({ For auth-enabled Collections, this hook runs after successful `forgotPassword` operations. Returned values are discarded. ```ts -import { CollectionAfterForgotPasswordHook } from "payload/types"; +import { CollectionAfterForgotPasswordHook } from 'payload/types' const afterLoginHook: CollectionAfterForgotPasswordHook = async ({ req, // full express request user, // user being logged in token, // user token }) => { - return user; -}; + return user +} ``` ## TypeScript @@ -319,5 +319,5 @@ import type { CollectionAfterRefreshHook, CollectionAfterMeHook, CollectionAfterForgotPasswordHook, -} from "payload/types"; +} from 'payload/types' ``` diff --git a/docs/hooks/context.mdx b/docs/hooks/context.mdx index 303a1b057..ed13616c3 100644 --- a/docs/hooks/context.mdx +++ b/docs/hooks/context.mdx @@ -31,24 +31,30 @@ For example: const Customer: CollectionConfig = { slug: 'customers', hooks: { - beforeChange: [async ({ context, data }) => { - // assign the customerData to context for use later - context.customerData = await fetchCustomerData(data.customerID); - return { - ...data, - // some data we use here - name: context.customerData.name - }; - }], - afterChange: [async ({ context, doc, req }) => { - // use context.customerData without needing to fetch it again - if (context.customerData.contacted === false) { - createTodo('Call Customer', context.customerData) - } - }], + beforeChange: [ + async ({ context, data }) => { + // assign the customerData to context for use later + context.customerData = await fetchCustomerData(data.customerID) + return { + ...data, + // some data we use here + name: context.customerData.name, + } + }, + ], + afterChange: [ + async ({ context, doc, req }) => { + // use context.customerData without needing to fetch it again + if (context.customerData.contacted === false) { + createTodo('Call Customer', context.customerData) + } + }, + ], }, - fields: [ /* ... */ ], -}; + fields: [ + /* ... */ + ], +} ``` ### Preventing infinite loops @@ -61,19 +67,23 @@ Bad example: const Customer: CollectionConfig = { slug: 'customers', hooks: { - afterChange: [async ({ doc }) => { - await payload.update({ - // DANGER: updating the same slug as the collection in an afterChange will create an infinite loop! - collection: 'customers', - id: doc.id, - data: { - ...(await fetchCustomerData(data.customerID)) - }, - }); - }], + afterChange: [ + async ({ doc }) => { + await payload.update({ + // DANGER: updating the same slug as the collection in an afterChange will create an infinite loop! + collection: 'customers', + id: doc.id, + data: { + ...(await fetchCustomerData(data.customerID)), + }, + }) + }, + ], }, - fields: [ /* ... */ ], -}; + fields: [ + /* ... */ + ], +} ``` Instead of the above, we need to tell the `afterChange` hook to not run again if it performs the update (and thus not update itself again). We can solve that with context. @@ -84,26 +94,30 @@ Fixed example: const MyCollection: CollectionConfig = { slug: 'slug', hooks: { - afterChange: [async ({ context, doc }) => { - // return if flag was previously set - if (context.triggerAfterChange === false) { - return; - } - await payload.update({ - collection: contextHooksSlug, - id: doc.id, - data: { - ...(await fetchCustomerData(data.customerID)) - }, - context: { - // set a flag to prevent from running again - triggerAfterChange: false, - }, - }); - }], + afterChange: [ + async ({ context, doc }) => { + // return if flag was previously set + if (context.triggerAfterChange === false) { + return + } + await payload.update({ + collection: contextHooksSlug, + id: doc.id, + data: { + ...(await fetchCustomerData(data.customerID)), + }, + context: { + // set a flag to prevent from running again + triggerAfterChange: false, + }, + }) + }, + ], }, - fields: [ /* ... */ ], -}; + fields: [ + /* ... */ + ], +} ``` ## Typing context @@ -113,12 +127,12 @@ The default typescript interface for `context` is `{ [key: string]: unknown }`. This is known as "type augmentation" - a TypeScript feature which allows us to add types to existing objects. Simply put this in any .ts or .d.ts file: ```ts -import { RequestContext as OriginalRequestContext } from 'payload'; +import { RequestContext as OriginalRequestContext } from 'payload' declare module 'payload' { // Create a new interface that merges your additional fields with the original one export interface RequestContext extends OriginalRequestContext { - myObject?: string; + myObject?: string // ... } } diff --git a/docs/hooks/fields.mdx b/docs/hooks/fields.mdx index 298e5e175..aec5d55aa 100644 --- a/docs/hooks/fields.mdx +++ b/docs/hooks/fields.mdx @@ -26,6 +26,7 @@ Field-level hooks offer incredible potential for encapsulating your logic. They ## Config Example field configuration: + ```ts import { Field } from 'payload/types'; @@ -48,8 +49,12 @@ const ExampleField: Field = { 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. - Tip:
- It's a good idea to conditionally scope your logic based on which operation is executing. For example, if you are writing a beforeChange hook, you may want to perform different logic based on if the current operation is create or update. + Tip: +
+ It's a good idea to conditionally scope your logic based on which operation is executing. For + example, if you are writing a beforeChange hook, you may want to perform + different logic based on if the current operation is create or{' '} + update.
#### Arguments @@ -57,7 +62,7 @@ All field-level hooks are formatted to accept the same arguments, although some Field Hooks receive one `args` argument that contains the following properties: | Option | Description | -|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`data`** | The data passed to update the document within `create` and `update` operations, and the full document itself in the `afterRead` hook. | | **`siblingData`** | The sibling data passed to a field that the hook is running against. | | **`findMany`** | Boolean to denote if this hook is running against finding one, or finding many within the `afterRead` hook. | @@ -74,8 +79,11 @@ Field Hooks receive one `args` argument that contains the following properties: 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. - Important
- Due to GraphQL's typed nature, you should never change the type of data that you return from a field, otherwise GraphQL will produce errors. If you need to change the shape or type of data, reconsider Field Hooks and instead evaluate if Collection / Global hooks might suit you better. + Important +
+ Due to GraphQL's typed nature, you should never change the type of data that you return from a + field, otherwise GraphQL will produce errors. If you need to change the shape or type of data, + reconsider Field Hooks and instead evaluate if Collection / Global hooks might suit you better.
## TypeScript @@ -83,14 +91,14 @@ All field hooks can optionally modify the return value of the field before the o Payload exports a type for field hooks which can be accessed and used as follows: ```ts -import type { FieldHook } from 'payload/types'; +import type { FieldHook } from 'payload/types' // Field hook type is a generic that takes three arguments: // 1: The document type // 2: The value type // 3: The sibling data type -type ExampleFieldHook = FieldHook; +type ExampleFieldHook = FieldHook const exampleFieldHook: ExampleFieldHook = (args) => { const { @@ -100,10 +108,10 @@ const exampleFieldHook: ExampleFieldHook = (args) => { originalDoc, // Typed as ExampleDocumentType operation, req, - } = args; + } = args // Do something here... - return value; // should return a string as typed above, undefined, or null + return value // should return a string as typed above, undefined, or null } ``` diff --git a/docs/hooks/globals.mdx b/docs/hooks/globals.mdx index 2b095c149..4314d3996 100644 --- a/docs/hooks/globals.mdx +++ b/docs/hooks/globals.mdx @@ -19,6 +19,7 @@ Globals feature the ability to define the following hooks: All Global Hook properties accept arrays of synchronous or asynchronous functions. Each Hook type receives specific arguments and has the ability to modify specific outputs. `globals/example-hooks.js` + ```ts import { GlobalConfig } from 'payload/types'; @@ -49,7 +50,7 @@ const beforeValidateHook: GlobalBeforeValidateHook = async ({ req, // full express request originalDoc, // original document }) => { - return data; // Return data to update the document with + return data // Return data to update the document with } ``` @@ -65,7 +66,7 @@ const beforeChangeHook: GlobalBeforeChangeHook = async ({ req, // full express request originalDoc, // original document }) => { - return data; // Return data to update the document with + return data // Return data to update the document with } ``` @@ -81,7 +82,7 @@ const afterChangeHook: GlobalAfterChangeHook = async ({ previousDoc, // document data before updating the collection req, // full express request }) => { - return data; + return data } ``` @@ -123,5 +124,5 @@ import type { GlobalAfterChangeHook, GlobalBeforeReadHook, GlobalAfterReadHook, -} from 'payload/types'; +} from 'payload/types' ``` diff --git a/docs/hooks/overview.mdx b/docs/hooks/overview.mdx index b27fe40cb..9f3f84b3b 100644 --- a/docs/hooks/overview.mdx +++ b/docs/hooks/overview.mdx @@ -7,7 +7,9 @@ keywords: hooks, overview, config, configuration, documentation, Content Managem --- - Hooks are powerful ways to tie into existing Payload actions in order to add your own logic like integrating with third-party APIs, adding auto-generated data, or modifing Payload's base functionality. + Hooks are powerful ways to tie into existing Payload actions in order to add your own logic like + integrating with third-party APIs, adding auto-generated data, or modifing Payload's base + functionality. **With Hooks, you can transform Payload from a traditional CMS into a fully-fledged application framework.** diff --git a/docs/integrations/vercel-visual-editing.mdx b/docs/integrations/vercel-visual-editing.mdx index 101b0100b..f06467c94 100644 --- a/docs/integrations/vercel-visual-editing.mdx +++ b/docs/integrations/vercel-visual-editing.mdx @@ -11,10 +11,9 @@ keywords: vercel, vercel visual editing, visual editing, content source maps, Co ![Versions](/images/docs/vercel-visual-editing.jpg) - Vercel Visual Editing is an enterprise-only feature and only available for - deployments hosted on Vercel. If you are an existing enterprise customer, - [contact our sales team](https://payloadcms.com/for-enterprise) for help with - your integration. + Vercel Visual Editing is an enterprise-only feature and only available for deployments hosted on + Vercel. If you are an existing enterprise customer, [contact our sales + team](https://payloadcms.com/for-enterprise) for help with your integration. ### How it works @@ -66,10 +65,10 @@ export default config Now in your Next.js app, include the `?encodeSourceMaps=true` parameter in any of your API requests. For performance reasons, this should only be done when in draft mode or on preview deployments. ```ts -if (isDraftMode || process.env.VERCEL_ENV === "preview") { +if (isDraftMode || process.env.VERCEL_ENV === 'preview') { const res = await fetch( - `${process.env.NEXT_PUBLIC_PAYLOAD_CMS_URL}/api/pages?where[slug][equals]=${slug}&encodeSourceMaps=true` - ); + `${process.env.NEXT_PUBLIC_PAYLOAD_CMS_URL}/api/pages?where[slug][equals]=${slug}&encodeSourceMaps=true`, + ) } ``` @@ -88,8 +87,8 @@ To see Visual Editing on your site, you first need to visit any preview deployme The plugin does not encode `date` fields by default, but for some cases like text that uses negative CSS letter-spacing, it may be necessary to split the encoded data out from the rendered text. This way you can safely use the cleaned data as expected. ```ts -import { vercelStegaSplit } from "@vercel/stega"; -const { cleaned, encoded } = vercelStegaSplit(text); +import { vercelStegaSplit } from '@vercel/stega' +const { cleaned, encoded } = vercelStegaSplit(text) ``` ##### Blocks diff --git a/docs/local-api/overview.mdx b/docs/local-api/overview.mdx index 142038117..21f3254d2 100644 --- a/docs/local-api/overview.mdx +++ b/docs/local-api/overview.mdx @@ -11,11 +11,10 @@ The Payload Local API gives you the ability to execute the same operations that Tip:
- The Local API is incredibly powerful when used with server-side rendering app - frameworks like NextJS. With other headless CMS, you need to request your data - from third-party servers which can add significant loading time to your - server-rendered pages. With Payload, you don't have to leave your server to - gather the data you need. It can be incredibly fast and is definitely a game + The Local API is incredibly powerful when used with server-side rendering app frameworks like + NextJS. With other headless CMS, you need to request your data from third-party servers which can + add significant loading time to your server-rendered pages. With Payload, you don't have to leave + your server to gather the data you need. It can be incredibly fast and is definitely a game changer.
@@ -36,14 +35,14 @@ You can import or require `payload` into your own files after it's been initiali Example: ```ts -import payload from "payload"; -import { CollectionAfterChangeHook } from "payload/types"; +import payload from 'payload' +import { CollectionAfterChangeHook } from 'payload/types' const afterChangeHook: CollectionAfterChangeHook = async () => { const posts = await payload.find({ - collection: "posts", - }); -}; + collection: 'posts', + }) +} ``` ##### Accessing from the `req` @@ -53,40 +52,37 @@ Payload is available anywhere you have access to the Express `req` - including w Example: ```ts -const afterChangeHook: CollectionAfterChangeHook = async ({ - req: { payload }, -}) => { +const afterChangeHook: CollectionAfterChangeHook = async ({ req: { payload } }) => { const posts = await payload.find({ - collection: "posts", - }); -}; + collection: 'posts', + }) +} ``` ### Local options available You can specify more options within the Local API vs. REST or GraphQL due to the server-only context that they are executed in. -| Local Option | Description | -| ------------------ | -------------------------------------------------------------------------------------------------------------------- | -| `collection` | Required for Collection operations. Specifies the Collection slug to operate against. | -| `data` | The data to use within the operation. Required for `create`, `update`. | -| `depth` | [Control auto-population](/docs/getting-started/concepts#depth) of nested relationship and upload fields. | -| `locale` | Specify [locale](/docs/configuration/localization) for any returned documents. | -| `fallbackLocale` | Specify a [fallback locale](/docs/configuration/localization) to use for any returned documents. | -| `overrideAccess` | Skip access control. By default, this property is set to true within all Local API operations. | -| `user` | If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks. | -| `showHiddenFields` | Opt-in to receiving hidden fields. By default, they are hidden from returned documents in accordance to your config. | -| `pagination` | Set to false to return all documents and avoid querying for document counts. | -| `context` | [Context](/docs/hooks/context), which will then be passed to `context` and `req.context`, which can be read by hooks. Useful if you want to pass additional information to the hooks which shouldn't be necessarily part of the document, for example a `triggerBeforeChange` option which can be read by the BeforeChange hook to determine if it should run or not. | +| Local Option | Description | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `collection` | Required for Collection operations. Specifies the Collection slug to operate against. | +| `data` | The data to use within the operation. Required for `create`, `update`. | +| `depth` | [Control auto-population](/docs/getting-started/concepts#depth) of nested relationship and upload fields. | +| `locale` | Specify [locale](/docs/configuration/localization) for any returned documents. | +| `fallbackLocale` | Specify a [fallback locale](/docs/configuration/localization) to use for any returned documents. | +| `overrideAccess` | Skip access control. By default, this property is set to true within all Local API operations. | +| `user` | If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks. | +| `showHiddenFields` | Opt-in to receiving hidden fields. By default, they are hidden from returned documents in accordance to your config. | +| `pagination` | Set to false to return all documents and avoid querying for document counts. | +| `context` | [Context](/docs/hooks/context), which will then be passed to `context` and `req.context`, which can be read by hooks. Useful if you want to pass additional information to the hooks which shouldn't be necessarily part of the document, for example a `triggerBeforeChange` option which can be read by the BeforeChange hook to determine if it should run or not. | _There are more options available on an operation by operation basis outlined below._ Note:
- By default, all access control checks are disabled in the Local API, but you - can re-enable them if you'd like, as well as pass a specific user to run the - operation with. + By default, all access control checks are disabled in the Local API, but you can re-enable them if + you'd like, as well as pass a specific user to run the operation with.
## Collections @@ -98,13 +94,13 @@ The following Collection operations are available through the Local API: ```js // The created Post document is returned const post = await payload.create({ - collection: "posts", // required + collection: 'posts', // required data: { // required - title: "sure", - description: "maybe", + title: 'sure', + description: 'maybe', }, - locale: "en", + locale: 'en', fallbackLocale: false, user: dummyUserDoc, overrideAccess: true, @@ -117,12 +113,12 @@ const post = await payload.create({ // If your collection supports uploads, you can upload // a file directly through the Local API by providing // its full, absolute file path. - filePath: path.resolve(__dirname, "./path-to-image.jpg"), + filePath: path.resolve(__dirname, './path-to-image.jpg'), // Alternatively, you can directly pass a File, // if file is provided, filePath will be omitted file: uploadedFile, -}); +}) ``` #### Find @@ -131,18 +127,18 @@ const post = await payload.create({ // Result will be a paginated set of Posts. // See /docs/queries/pagination for more. const result = await payload.find({ - collection: "posts", // required + collection: 'posts', // required depth: 2, page: 1, limit: 10, where: {}, // pass a `where` query here - sort: "-title", - locale: "en", + sort: '-title', + locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, -}); +}) ``` #### Find by ID @@ -150,15 +146,15 @@ const result = await payload.find({ ```js // Result will be a Post document. const result = await payload.findByID({ - collection: "posts", // required - id: "507f1f77bcf86cd799439011", // required + collection: 'posts', // required + id: '507f1f77bcf86cd799439011', // required depth: 2, - locale: "en", + locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, -}); +}) ``` #### Update by ID @@ -166,15 +162,15 @@ const result = await payload.findByID({ ```js // Result will be the updated Post document. const result = await payload.update({ - collection: "posts", // required - id: "507f1f77bcf86cd799439011", // required + collection: 'posts', // required + id: '507f1f77bcf86cd799439011', // required data: { // required - title: "sure", - description: "maybe", + title: 'sure', + description: 'maybe', }, depth: 2, - locale: "en", + locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, @@ -183,13 +179,13 @@ const result = await payload.update({ // If your collection supports uploads, you can upload // a file directly through the Local API by providing // its full, absolute file path. - filePath: path.resolve(__dirname, "./path-to-image.jpg"), + filePath: path.resolve(__dirname, './path-to-image.jpg'), // If you are uploading a file and would like to replace // the existing file instead of generating a new filename, // you can set the following property to `true` overwriteExistingFiles: true, -}); +}) ``` #### Update Many @@ -201,18 +197,18 @@ const result = await payload.update({ // errors: [], // each error also includes the id of the document // } const result = await payload.update({ - collection: "posts", // required + collection: 'posts', // required where: { // required - fieldName: { equals: "value" }, + fieldName: { equals: 'value' }, }, data: { // required - title: "sure", - description: "maybe", + title: 'sure', + description: 'maybe', }, depth: 0, - locale: "en", + locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, @@ -221,13 +217,13 @@ const result = await payload.update({ // If your collection supports uploads, you can upload // a file directly through the Local API by providing // its full, absolute file path. - filePath: path.resolve(__dirname, "./path-to-image.jpg"), + filePath: path.resolve(__dirname, './path-to-image.jpg'), // If you are uploading a file and would like to replace // the existing file instead of generating a new filename, // you can set the following property to `true` overwriteExistingFiles: true, -}); +}) ``` #### Delete @@ -235,15 +231,15 @@ const result = await payload.update({ ```js // Result will be the now-deleted Post document. const result = await payload.delete({ - collection: "posts", // required - id: "507f1f77bcf86cd799439011", // required + collection: 'posts', // required + id: '507f1f77bcf86cd799439011', // required depth: 2, - locale: "en", + locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, -}); +}) ``` #### Delete Many @@ -255,18 +251,18 @@ const result = await payload.delete({ // errors: [], // any errors that occurred, including the id of the errored on document // } const result = await payload.delete({ - collection: "posts", // required + collection: 'posts', // required where: { // required - fieldName: { equals: "value" }, + fieldName: { equals: 'value' }, }, depth: 0, - locale: "en", + locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, -}); +}) ``` ## Auth Operations @@ -284,20 +280,20 @@ If a collection has [`Authentication`](/docs/authentication/overview) enabled, a // } const result = await payload.login({ - collection: "users", // required + collection: 'users', // required data: { // required - email: "dev@payloadcms.com", - password: "rip", + email: 'dev@payloadcms.com', + password: 'rip', }, req: req, // pass an Express `req` which will be provided to all hooks res: res, // used to automatically set an HTTP-only auth cookie depth: 2, - locale: "en", + locale: 'en', fallbackLocale: false, overrideAccess: false, showHiddenFields: true, -}); +}) ``` #### Forgot Password @@ -305,13 +301,13 @@ const result = await payload.login({ ```js // Returned token will allow for a password reset const token = await payload.forgotPassword({ - collection: "users", // required + collection: 'users', // required data: { // required - email: "dev@payloadcms.com", + email: 'dev@payloadcms.com', }, req: req, // pass an Express `req` which will be provided to all hooks -}); +}) ``` #### Reset Password @@ -323,14 +319,14 @@ const token = await payload.forgotPassword({ // user: { ... } // the user document that just logged in // } const result = await payload.forgotPassword({ - collection: "users", // required + collection: 'users', // required data: { // required - token: "afh3o2jf2p3f...", // the token generated from the forgotPassword operation + token: 'afh3o2jf2p3f...', // the token generated from the forgotPassword operation }, req: req, // pass an Express `req` which will be provided to all hooks res: res, // used to automatically set an HTTP-only auth cookie -}); +}) ``` #### Unlock @@ -338,14 +334,14 @@ const result = await payload.forgotPassword({ ```js // Returned result will be a boolean representing success or failure const result = await payload.unlock({ - collection: "users", // required + collection: 'users', // required data: { // required - email: "dev@payloadcms.com", + email: 'dev@payloadcms.com', }, req: req, // pass an Express `req` which will be provided to all hooks overrideAccess: true, -}); +}) ``` #### Verify @@ -353,9 +349,9 @@ const result = await payload.unlock({ ```js // Returned result will be a boolean representing success or failure const result = await payload.verify({ - collection: "users", // required - token: "afh3o2jf2p3f...", // the token saved on the user as `_verificationToken` -}); + collection: 'users', // required + token: 'afh3o2jf2p3f...', // the token saved on the user as `_verificationToken` +}) ``` ## Globals @@ -367,14 +363,14 @@ The following Global operations are available through the Local API: ```js // Result will be the Header Global. const result = await payload.findGlobal({ - slug: "header", // required + slug: 'header', // required depth: 2, - locale: "en", + locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, -}); +}) ``` #### Update @@ -382,25 +378,25 @@ const result = await payload.findGlobal({ ```js // Result will be the updated Header Global. const result = await payload.updateGlobal({ - slug: "header", // required + slug: 'header', // required data: { // required nav: [ { - url: "https://google.com", + url: 'https://google.com', }, { - url: "https://payloadcms.com", + url: 'https://payloadcms.com', }, ], }, depth: 2, - locale: "en", + locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, -}); +}) ``` ## Example Script using Local API @@ -408,36 +404,36 @@ const result = await payload.updateGlobal({ The Local API is especially useful for running scripts ```ts -import payload from "payload"; -import path from "path"; -import dotenv from "dotenv"; +import payload from 'payload' +import path from 'path' +import dotenv from 'dotenv' dotenv.config({ - path: path.resolve(__dirname, "../.env"), -}); + path: path.resolve(__dirname, '../.env'), +}) -const { PAYLOAD_SECRET, MONGODB_URI } = process.env; +const { PAYLOAD_SECRET, MONGODB_URI } = process.env const doAction = async (): Promise => { await payload.init({ secret: PAYLOAD_SECRET, mongoURL: MONGODB_URI, local: true, // Enables local mode, doesn't spin up a server or frontend - }); + }) // Perform any Local API operations here await payload.find({ - collection: "posts", + collection: 'posts', // where: {} // optional - }); + }) await payload.create({ - collection: "posts", + collection: 'posts', data: {}, - }); -}; + }) +} -doAction(); +doAction() ``` ## TypeScript @@ -449,12 +445,12 @@ Here is an example of usage: ```ts // Properly inferred as `Post` type const post = await payload.create({ - collection: "posts", + collection: 'posts', // Data will now be typed as Post and give you type hints data: { - title: "my title", - description: "my description", + title: 'my title', + description: 'my description', }, -}); +}) ``` diff --git a/docs/plugins/overview.mdx b/docs/plugins/overview.mdx index 82578a21e..75d526db0 100644 --- a/docs/plugins/overview.mdx +++ b/docs/plugins/overview.mdx @@ -9,7 +9,9 @@ keywords: plugins, config, configuration, extensions, custom, documentation, Con Payload comes with a built-in Plugins infrastructure that allows developers to build their own modular and easily reusable sets of functionality. - Because we rely on a simple config-based structure, Payload plugins simply take in a user's existing config and return a modified config with new fields, hooks, collections, admin views, or anything else you can think of. + Because we rely on a simple config-based structure, Payload plugins simply take in a user's + existing config and return a modified config with new fields, hooks, collections, admin views, or + anything else you can think of. Writing plugins is no more complex than writing regular JavaScript. If you know how [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) works and are up to speed with Payload concepts, writing a plugin will be a breeze. @@ -30,30 +32,30 @@ Writing plugins is no more complex than writing regular JavaScript. If you know The base Payload config allows for a `plugins` property which takes an `array` of [`Plugins`](https://github.com/payloadcms/payload/blob/master/src/config/types.ts#L21). ```js -import { buildConfig } from 'payload/config'; +import { buildConfig } from 'payload/config' // note: these plugins are not real (yet?) -import addLastModified from 'payload-add-last-modified'; -import passwordProtect from 'payload-password-protect'; +import addLastModified from 'payload-add-last-modified' +import passwordProtect from 'payload-password-protect' const config = buildConfig({ - collections: [ - { - slug: 'pages', - fields: [ - { - name: 'title', - type: 'text', - required: true, - }, - { - name: 'content', - type: 'richText', - required: true, - } - ] - } - ], + collections: [ + { + slug: 'pages', + fields: [ + { + name: 'title', + type: 'text', + required: true, + }, + { + name: 'content', + type: 'richText', + required: true, + }, + ], + }, + ], plugins: [ // Many plugins require options to be passed. // In the following example, we call the function @@ -67,10 +69,10 @@ const config = buildConfig({ // .. // To understand how to use the plugins you're interested in, // consult their corresponding documentation - ] -}); + ], +}) -export default config; +export default config ``` #### When Plugins are initialized @@ -84,21 +86,20 @@ After all plugins are executed, the full config with all plugins will be sanitiz Here is an example for how to automatically add a `lastModifiedBy` field to all Payload collections using a Plugin written in TypeScript. ```ts -import { Config, Plugin } from 'payload/config'; +import { Config, Plugin } from 'payload/config' const addLastModified: Plugin = (incomingConfig: Config): Config => { // Find all incoming auth-enabled collections // so we can create a lastModifiedBy relationship field // to all auth collections - const authEnabledCollections = incomingConfig.collections.filter( - collection => Boolean(collection.auth) - ); + const authEnabledCollections = incomingConfig.collections.filter((collection) => + Boolean(collection.auth), + ) // Spread the existing config const config: Config = { ...incomingConfig, collections: incomingConfig.collections.map((collection) => { - // Spread each item that we are modifying, // and add our new field - complete with // hooks and proper admin UI config @@ -116,7 +117,7 @@ const addLastModified: Plugin = (incomingConfig: Config): Config => { value: req?.user?.id, relationTo: req?.user?.collection, }), - ] + ], }, admin: { position: 'sidebar', @@ -124,14 +125,14 @@ const addLastModified: Plugin = (incomingConfig: Config): Config => { }, }, ], - }; + } }), - }; + } - return config; -}; + return config +} -export default addLastModified; +export default addLastModified ``` ### Available Plugins diff --git a/docs/production/deployment.mdx b/docs/production/deployment.mdx index e7b5fab6b..15a77ab20 100644 --- a/docs/production/deployment.mdx +++ b/docs/production/deployment.mdx @@ -7,7 +7,8 @@ keywords: deployment, production, config, configuration, documentation, Content --- - So you've developed a Payload app, it's fully tested, and running great locally. Now it's time to launch. Awesome! Great work! Now, what's next? + So you've developed a Payload app, it's fully tested, and running great locally. Now it's time to + launch. Awesome! Great work! Now, what's next? There are many ways to deploy Payload to a production environment. When evaluating how you will deploy Payload, you need to consider these main aspects: @@ -35,7 +36,13 @@ When you initialize Payload, you provide it with a `secret` property. This prope Because _**you**_ are in complete control of who can do what with your data, you should double and triple-check that you wield that power responsibly before deploying to Production. - By default, all Access Control functions require that a user is successfully logged in to Payload to create, read, update, or delete data. But, if you allow public user registration, for example, you will want to make sure that your access control functions are more strict - permitting only appropriate users to perform appropriate actions. + + By default, all Access Control functions require that a user is successfully logged in to + Payload to create, read, update, or delete data. + {' '} + But, if you allow public user registration, for example, you will want to make sure that your + access control functions are more strict - permitting only appropriate users to + perform appropriate actions. ##### Building the Admin panel @@ -83,8 +90,13 @@ If you are using a [persistent filesystem-based cloud host](#persistent-vs-ephem Alternatively, you can rely on a third-party MongoDB host such as [MongoDB Atlas](https://www.mongodb.com/). With Atlas or a similar cloud provider, you can trust them to take care of your database's availability, security, redundancy, and backups. - Note:
- If versions are enabled and a collection has many documents you may need a minimum of an m10 mongoDB atlas cluster if you reach a sorting `exceeded memory limit` error to view a collection list in the admin UI. The limitations of the m2 and m5 tier clusters are here: [Atlas M0 (Free Cluster), M2, and M5 Limitations](https://www.mongodb.com/docs/atlas/reference/free-shared-limitations/?_ga=2.176267877.1329169847.1677683154-860992573.1647438381#operational-limitations). + Note: +
+ If versions are enabled and a collection has many documents you may need a minimum of an m10 + mongoDB atlas cluster if you reach a sorting `exceeded memory limit` error to view a collection + list in the admin UI. The limitations of the m2 and m5 tier clusters are here: [Atlas M0 (Free + Cluster), M2, and M5 + Limitations](https://www.mongodb.com/docs/atlas/reference/free-shared-limitations/?_ga=2.176267877.1329169847.1677683154-860992573.1647438381#operational-limitations).
##### DocumentDB @@ -118,8 +130,10 @@ Alternatively, persistent filesystems will never delete your files and can be tr - Many other more traditional web hosts - Warning:
- If you rely on Payload's Upload functionality, make sure you either use a host with a persistent filesystem or have an integration with a third-party file host like Amazon S3. + Warning: +
+ If you rely on Payload's Upload functionality, make sure you either use a host + with a persistent filesystem or have an integration with a third-party file host like Amazon S3.
##### Using ephemeral filesystem providers like Heroku @@ -184,13 +198,13 @@ CMD ["node", "dist/server.js"] Here is an example of a docker-compose.yml file that can be used for development ```yml -version: "3" +version: '3' services: payload: image: node:18-alpine ports: - - "3000:3000" + - '3000:3000' volumes: - .:/home/node/app - node_modules:/home/node/app/node_modules @@ -207,7 +221,7 @@ services: mongo: image: mongo:latest ports: - - "27017:27017" + - '27017:27017' command: - --storageEngine=wiredTiger volumes: diff --git a/docs/production/preventing-abuse.mdx b/docs/production/preventing-abuse.mdx index 5cc0b7dbc..a83820b5b 100644 --- a/docs/production/preventing-abuse.mdx +++ b/docs/production/preventing-abuse.mdx @@ -18,16 +18,21 @@ Set the max number of failed login attempts before a user account is locked out To prevent DDoS, brute-force, and similar attacks, you can set IP-based rate limits so that once a certain threshold of requests has been hit by a single IP, further requests from the same IP will be ignored. The Payload config `rateLimit` property accepts an object with the following properties: -| Option | Description | -| ---------------------------- | ----------- | -| **`window`** | Time in milliseconds to track requests per IP. Defaults to `90000` (15 minutes). | -| **`max`** | Number of requests served from a single IP before limiting. Defaults to `500`. | -| **`skip`** | Express middleware function that can return true (or promise resulting in true) that will bypass limit. | -| **`trustProxy`** | True or false, to enable to allow requests to pass through a proxy such as a load balancer or an `nginx` reverse proxy. | +| Option | Description | +| ---------------- | ----------------------------------------------------------------------------------------------------------------------- | +| **`window`** | Time in milliseconds to track requests per IP. Defaults to `90000` (15 minutes). | +| **`max`** | Number of requests served from a single IP before limiting. Defaults to `500`. | +| **`skip`** | Express middleware function that can return true (or promise resulting in true) that will bypass limit. | +| **`trustProxy`** | True or false, to enable to allow requests to pass through a proxy such as a load balancer or an `nginx` reverse proxy. | - Warning:
- Very commonly, NodeJS apps are served behind `nginx` reverse proxies and similar. If you use rate-limiting while you're behind a proxy, all IP addresses from everyone that uses your API will appear as if they are from a local origin (127.0.0.1), and your users will get rate-limited very quickly without cause. If you plan to host your app behind a proxy, make sure you set trustProxy to true. + Warning: +
+ Very commonly, NodeJS apps are served behind `nginx` reverse proxies and similar. If you use + rate-limiting while you're behind a proxy, all IP addresses from everyone that + uses your API will appear as if they are from a local origin (127.0.0.1), and your users will get + rate-limited very quickly without cause. If you plan to host your app behind a proxy, make sure + you set trustProxy to true.
### Max Depth diff --git a/docs/queries/overview.mdx b/docs/queries/overview.mdx index bdfabad6e..d54148159 100644 --- a/docs/queries/overview.mdx +++ b/docs/queries/overview.mdx @@ -9,7 +9,11 @@ keywords: query, documents, overview, documentation, Content Management System, Payload provides an extremely granular querying language through all APIs. Each API takes the same syntax and fully supports all options. - Here, "querying" relates to filtering or searching through documents within a Collection. You can build queries to pass to Find operations as well as to [restrict which documents certain users can access](/docs/access-control/overview) via access control functions. + + Here, "querying" relates to filtering or searching through documents within a Collection. + {' '} + You can build queries to pass to Find operations as well as to [restrict which documents certain + users can access](/docs/access-control/overview) via access control functions. ### Simple queries @@ -17,22 +21,22 @@ Payload provides an extremely granular querying language through all APIs. Each For example, say you have a collection as follows: ```ts -import { CollectionConfig } from "payload/types"; +import { CollectionConfig } from 'payload/types' export const Post: CollectionConfig = { - slug: "posts", + slug: 'posts', fields: [ { - name: "color", - type: "select", - options: ["mint", "dark-gray", "white"], + name: 'color', + type: 'select', + options: ['mint', 'dark-gray', 'white'], }, { - name: "featured", - type: "checkbox", + name: 'featured', + type: 'checkbox', }, ], -}; +} ``` You may eventually have a lot of documents within this Collection. If you wanted to find only documents with `color` equal to `mint`, you could write a query as follows: @@ -41,9 +45,9 @@ You may eventually have a lot of documents within this Collection. If you wanted const query = { color: { // property name to filter on - equals: "mint", // operator to use and value to compare against + equals: 'mint', // operator to use and value to compare against }, -}; +} ``` The above example demonstrates a simple query but you can get much more complex. @@ -68,7 +72,9 @@ The above example demonstrates a simple query but you can get much more complex. Tip:
- If you know your users will be querying on certain fields a lot, you can add index: true to a field's config which will speed up searches using that field immensely. + If you know your users will be querying on certain fields a lot, you can add + index: true + to a field's config which will speed up searches using that field immensely.
### And / Or Logic @@ -81,7 +87,7 @@ const query = { // array of OR conditions { color: { - equals: "mint", + equals: 'mint', }, }, { @@ -89,7 +95,7 @@ const query = { // nested array of AND conditions { color: { - equals: "white", + equals: 'white', }, }, { @@ -100,7 +106,7 @@ const query = { ], }, ], -}; +} ``` Written in plain English, if the above query were passed to a `find` operation, it would translate to finding posts where either the `color` is `mint` OR the `color` is `white` AND `featured` is set to false. @@ -111,11 +117,11 @@ When working with nested properties, which can happen when using relational fiel ```js const query = { - "artists.featured": { + 'artists.featured': { // nested property name to filter on exists: true, // operator to use and boolean value that needs to be true }, -}; +} ``` ### GraphQL Find Queries @@ -148,29 +154,27 @@ This one isn't too bad, but more complex queries get unavoidably more difficult **For example, using fetch:** ```js -import qs from "qs"; +import qs from 'qs' const query = { color: { - equals: "mint", + equals: 'mint', }, // This query could be much more complex // and QS would handle it beautifully -}; +} const getPosts = async () => { const stringifiedQuery = qs.stringify( { where: query, // ensure that `qs` adds the `where` property, too! }, - { addQueryPrefix: true } - ); + { addQueryPrefix: true }, + ) - const response = await fetch( - `http://localhost:3000/api/posts${stringifiedQuery}` - ); + const response = await fetch(`http://localhost:3000/api/posts${stringifiedQuery}`) // Continue to handle the response below... -}; +} ``` ### Local API Queries @@ -180,16 +184,16 @@ The Local API's `find` operation accepts an object exactly how you write it. For ```js const getPosts = async () => { const posts = await payload.find({ - collection: "posts", + collection: 'posts', where: { color: { - equals: "mint", + equals: 'mint', }, }, - }); + }) - return posts; -}; + return posts +} ``` ## Sort @@ -216,10 +220,10 @@ query { ```js const getPosts = async () => { const posts = await payload.find({ - collection: "posts", - sort: "-createdAt", - }); + collection: 'posts', + sort: '-createdAt', + }) - return posts; -}; + return posts +} ``` diff --git a/docs/queries/pagination.mdx b/docs/queries/pagination.mdx index ef9cbd299..67215cf9c 100644 --- a/docs/queries/pagination.mdx +++ b/docs/queries/pagination.mdx @@ -10,20 +10,21 @@ All collection `find` queries are paginated automatically. Responses are returne **`Find` response properties:** -| Property | Description | -| ------------- | ---------------------------------------------------------- | -| docs | Array of documents in the collection | -| totalDocs | Total available documents within the collection | -| limit | Limit query parameter - defaults to `10` | -| totalPages | Total pages available, based upon the `limit` queried for | -| page | Current page number | -| pagingCounter | `number` of the first doc on the current page | -| hasPrevPage | `true/false` if previous page exists | -| hasNextPage | `true/false` if next page exists | -| prevPage | `number` of previous page, `null` if it doesn't exist | -| nextPage | `number` of next page, `null` if it doesn't exist | +| Property | Description | +| ------------- | --------------------------------------------------------- | +| docs | Array of documents in the collection | +| totalDocs | Total available documents within the collection | +| limit | Limit query parameter - defaults to `10` | +| totalPages | Total pages available, based upon the `limit` queried for | +| page | Current page number | +| pagingCounter | `number` of the first doc on the current page | +| hasPrevPage | `true/false` if previous page exists | +| hasNextPage | `true/false` if next page exists | +| prevPage | `number` of previous page, `null` if it doesn't exist | +| nextPage | `number` of next page, `null` if it doesn't exist | **Example response:** + ```json { // Document Array // highlight-line @@ -54,7 +55,7 @@ All collection `find` queries are paginated automatically. Responses are returne All Payload APIs support the pagination controls below. With them, you can create paginated lists of documents within your application: -| Control | Description | -| --------- | ----------------------------------------------------------------------------------------- | -| `limit` | Limits the number of documents returned | -| `page` | Get a specific page number | +| Control | Description | +| ------- | --------------------------------------- | +| `limit` | Limits the number of documents returned | +| `page` | Get a specific page number | diff --git a/docs/rest-api/overview.mdx b/docs/rest-api/overview.mdx index 81d9f65bb..bafd594b9 100644 --- a/docs/rest-api/overview.mdx +++ b/docs/rest-api/overview.mdx @@ -7,8 +7,7 @@ keywords: rest, api, documentation, Content Management System, cms, headless, ja --- - A fully functional REST API is automatically generated from your Collection - and Global configs. + A fully functional REST API is automatically generated from your Collection and Global configs. All Payload API routes are mounted prefixed to your config's `routes.api` URL segment (default: `/api`). @@ -439,45 +438,45 @@ Globals cannot be created or deleted, so there are only two REST endpoints opene { - const tracking = await getTrackingInfo(req.params.id); + const tracking = await getTrackingInfo(req.params.id) if (tracking) { - res.status(200).send({ tracking }); + res.status(200).send({ tracking }) } else { - res.status(404).send({ error: "not found" }); + res.status(404).send({ error: 'not found' }) } }, }, ], // highlight-end -}; +} ``` Note:
- **req** will have the **payload** object and can be used inside your endpoint - handlers for making calls like req.payload.find() that will make use of access - control and hooks. + **req** will have the **payload** object and can be used inside your endpoint handlers for making + calls like req.payload.find() that will make use of access control and hooks.
diff --git a/docs/troubleshooting/troubleshooting.mdx b/docs/troubleshooting/troubleshooting.mdx index ee2d811a4..51c65ff64 100644 --- a/docs/troubleshooting/troubleshooting.mdx +++ b/docs/troubleshooting/troubleshooting.mdx @@ -12,7 +12,7 @@ keywords: admin, components, custom, customize, documentation, Content Managemen This means that your auth cookie is not being set or accepted correctly upon logging in. To resolve heck the following settings in your Payload config: -- CORS - If you are using the '*', try to explicitly only allow certain domains instead including the one you have specified. +- CORS - If you are using the '\*', try to explicitly only allow certain domains instead including the one you have specified. - CSRF - Do you have this set? if so, make sure your domain is whitelisted within the csrf domains. If not, probably not the issue, but probably can't hurt to whitelist it anyway. - Cookie settings. If these are completely undefined, then that's fine. but if you have cookie domain set, or anything similar, make sure you don't have the domain misconfigured diff --git a/docs/typescript/generating-types.mdx b/docs/typescript/generating-types.mdx index 0618dd50d..900bd1a55 100644 --- a/docs/typescript/generating-types.mdx +++ b/docs/typescript/generating-types.mdx @@ -102,19 +102,19 @@ By generating types, we'll end up with a file containing the following two TypeS ```ts export interface User { - id: string; - name: string; - email?: string; - resetPasswordToken?: string; - resetPasswordExpiration?: string; - loginAttempts?: number; - lockUntil?: string; + id: string + name: string + email?: string + resetPasswordToken?: string + resetPasswordExpiration?: string + loginAttempts?: number + lockUntil?: string } export interface Post { - id: string; - title?: string; - author?: string | User; + id: string + title?: string + author?: string | User } ``` @@ -145,25 +145,24 @@ will generate: ```ts // a top level reusable interface!! export interface SharedMeta { - title?: string; - description?: string; + title?: string + description?: string } // example usage inside collection interface export interface Collection1 { // ...other fields - meta?: SharedMeta; + meta?: SharedMeta } ``` Naming Collisions
- Since these types are hoisted to the top level, you need to be aware that - naming collisions can occur. For example, if you have a collection with the - name of `Meta` and you also create a interface with the name `Meta` they will - collide. It is recommended to scope your interfaces by appending the field - type to the end, i.e. `MetaGroup` or similar. + Since these types are hoisted to the top level, you need to be aware that naming collisions can + occur. For example, if you have a collection with the name of `Meta` and you also create a + interface with the name `Meta` they will collide. It is recommended to scope your interfaces by + appending the field type to the end, i.e. `MetaGroup` or similar.
### Using your types diff --git a/docs/upload/overview.mdx b/docs/upload/overview.mdx index 65f1334ae..1e5f2f6d9 100644 --- a/docs/upload/overview.mdx +++ b/docs/upload/overview.mdx @@ -7,9 +7,8 @@ keywords: uploads, images, media, overview, documentation, Content Management Sy --- - Payload provides for everything you need to enable file upload, storage, and - management directly on your server—including extremely powerful file access - control. + Payload provides for everything you need to enable file upload, storage, and management directly + on your server—including extremely powerful file access control. ![Upload admin panel functionality](https://payloadcms.com/images/docs/upload-admin.jpg) @@ -34,31 +33,32 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl Tip: -
A common pattern is to create a Media collection and enable{" "} - upload on that collection. +
A common pattern is to create a Media collection and enable + upload + on that collection.
#### Collection Upload Options -| Option | Description | -| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Option | Description | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`staticURL`** \* | The URL path to use to access your uploads. Relative path like `/media` will be served by payload. Full path like `https://example.com/media` needs to be served by another web server. | -| **`staticDir`** \* | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. | -| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) | -| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) | -| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) | -| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) | -| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) | -| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) | -| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) | -| **`handlers`** | Array of Express request handlers to execute before the built-in Payload static middleware executes. | +| **`staticDir`** \* | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. | +| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) | +| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) | +| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) | +| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) | +| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) | +| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) | +| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) | +| **`handlers`** | Array of Express request handlers to execute before the built-in Payload static middleware executes. | _An asterisk denotes that a property above is required._ **Example Upload collection:** ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const Media: CollectionConfig = { slug: 'media', @@ -98,7 +98,7 @@ export const Media: CollectionConfig = { type: 'text', }, ], -}; +} ``` ### Payload-wide Upload Options @@ -108,7 +108,7 @@ Payload relies on the [`express-fileupload`](https://www.npmjs.com/package/expre A common example of what you might want to customize within Payload-wide Upload options would be to increase the allowed `fileSize` of uploads sent to Payload: ```ts -import { buildConfig } from 'payload/config'; +import { buildConfig } from 'payload/config' export default buildConfig({ collections: [ @@ -128,7 +128,7 @@ export default buildConfig({ fileSize: 5000000, // 5MB, written in bytes }, }, -}); +}) ``` ### Image Sizes @@ -152,11 +152,10 @@ If you are using a plugin to send your files off to a third-party file storage h Note:
- This is a fairly advanced feature. If you do disable local file storage, by - default, your admin panel's thumbnails will be broken as you will not have - stored a file. It will be totally up to you to use either a plugin or your own - hooks to store your files in a permanent manner, as well as provide your own - admin thumbnail using upload.adminThumbnail. + This is a fairly advanced feature. If you do disable local file storage, by default, your admin + panel's thumbnails will be broken as you will not have stored a file. It will be totally up to you + to use either a plugin or your own hooks to store your files in a permanent manner, as well as + provide your own admin thumbnail using upload.adminThumbnail.
### Admin Thumbnails @@ -169,7 +168,7 @@ You can specify how Payload retrieves admin thumbnails for your upload-enabled C **Example custom Admin thumbnail:** ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const Media: CollectionConfig = { slug: 'media', @@ -180,17 +179,17 @@ export const Media: CollectionConfig = { // ... image sizes here ], // highlight-start - adminThumbnail: ({ doc }) => - `https://google.com/custom-path-to-file/${doc.filename}`, + adminThumbnail: ({ doc }) => `https://google.com/custom-path-to-file/${doc.filename}`, // highlight-end }, -}; +} ``` Note:
- This function runs in the browser. If your function returns `null` or `false` Payload will show the default generic file thumbnail instead. + This function runs in the browser. If your function returns `null` or `false` Payload will show + the default generic file thumbnail instead.
### MimeTypes @@ -202,7 +201,7 @@ Some example values are: `image/*`, `audio/*`, `video/*`, `image/png`, `applicat **Example mimeTypes usage:** ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const Media: CollectionConfig = { slug: 'media', @@ -211,7 +210,7 @@ export const Media: CollectionConfig = { staticDir: 'media', mimeTypes: ['image/*', 'application/pdf'], // highlight-line }, -}; +} ``` ### Uploading Files @@ -219,9 +218,8 @@ export const Media: CollectionConfig = { Important:
- Uploading files is currently only possible through the REST and Local APIs due - to how GraphQL works. It's difficult and fairly nonsensical to support - uploading files through GraphQL. + Uploading files is currently only possible through the REST and Local APIs due to how GraphQL + works. It's difficult and fairly nonsensical to support uploading files through GraphQL.
To upload a file, use your collection's [`create`](/docs/rest-api/overview#collections) endpoint. Send it all the data that your Collection requires, as well as a `file` key containing the file that you'd like to upload. diff --git a/docs/versions/autosave.mdx b/docs/versions/autosave.mdx index a01ea4f1c..f5edcb777 100644 --- a/docs/versions/autosave.mdx +++ b/docs/versions/autosave.mdx @@ -9,24 +9,24 @@ keywords: version history, revisions, audit log, draft, publish, autosave, Conte Extending on Payload's [Draft](/docs/versions/drafts) functionality, you can configure your collections and globals to autosave changes as drafts, and publish only you're ready. The Admin UI will automatically adapt to autosaving progress at an interval that you define, and will store all autosaved changes as a new Draft version. Never lose your work - and publish changes to the live document only when you're ready. - Autosave relies on Versions and Drafts being enabled in order to function. + Autosave relies on Versions and Drafts being enabled in order to function. ![Autosave Enabled](/images/docs/autosave-enabled.png) -*If Autosave is enabled, drafts will be created automatically as the document is modified and the Admin UI adds an indicator describing when the document was last saved to the top right of the sidebar.* +_If Autosave is enabled, drafts will be created automatically as the document is modified and the Admin UI adds an indicator describing when the document was last saved to the top right of the sidebar._ ### Options Collections and Globals both support the same options for configuring autosave. You can either set `versions.drafts.autosave` to `true`, or pass an object to configure autosave properties. -| Drafts Autosave Options | Description | -| ---------------------------- | -------------| -| `interval` | Define an `interval` in milliseconds to automatically save progress while documents are edited. Document updates are "debounced" at this interval. Defaults to `2000`. | +| Drafts Autosave Options | Description | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `interval` | Define an `interval` in milliseconds to automatically save progress while documents are edited. Document updates are "debounced" at this interval. Defaults to `2000`. | **Example config with versions, drafts, and autosave enabled:** ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const Pages: CollectionConfig = { slug: 'pages', @@ -34,7 +34,7 @@ export const Pages: CollectionConfig = { read: ({ req }) => { // If there is a user logged in, // let them retrieve all documents - if (req.user) return true; + if (req.user) return true // If there is no user, // restrict the documents that are returned @@ -43,7 +43,7 @@ export const Pages: CollectionConfig = { _status: { equals: 'published', }, - }; + } }, }, versions: { @@ -54,7 +54,7 @@ export const Pages: CollectionConfig = { // autosave: { // interval: 1500, // }, - } + }, }, //.. the rest of the Pages config here } @@ -69,5 +69,7 @@ When `autosave` is enabled, all `update` operations within Payload expose a new If we created a new version for each autosave, you'd quickly find a ton of autosaves that clutter up your `_versions` collection within the database. That would be messy quick because `autosave` is typically set to save a document every ~2000ms or so. - Instead of creating a new version each time a document is autosaved, Payload smartly only creates one autosave version, and then updates that specific version with each autosave performed. This makes sure that your versions remain nice and tidy. + Instead of creating a new version each time a document is autosaved, Payload smartly only creates{' '} + one autosave version, and then updates that specific version with each autosave + performed. This makes sure that your versions remain nice and tidy. diff --git a/docs/versions/drafts.mdx b/docs/versions/drafts.mdx index af08dcc26..5b850ff6a 100644 --- a/docs/versions/drafts.mdx +++ b/docs/versions/drafts.mdx @@ -8,22 +8,20 @@ keywords: version history, drafts, preview, draft, restore, publish, autosave, C Payload's Draft functionality builds on top of the Versions functionality to allow you to make changes to your collection documents and globals, but publish only when you're ready. This functionality allows you to build powerful Preview environments for your data, where you can make sure your changes look good before publishing documents. - - Drafts rely on Versions being enabled in order to function. - +Drafts rely on Versions being enabled in order to function. By enabling Versions with Drafts, your collections and globals can maintain _newer_, and _unpublished_ versions of your documents. It's perfect for cases where you might want to work on a document, update it and save your progress, but not necessarily make it publicly published right away. Drafts are extremely helpful when building preview implementations. ![Drafts Enabled](/images/docs/drafts-enabled.png) -*If Drafts are enabled, the typical Save button is replaced with new actions which allow you to either save a draft, or publish your changes.* +_If Drafts are enabled, the typical Save button is replaced with new actions which allow you to either save a draft, or publish your changes._ ### Options Collections and Globals both support the same options for configuring drafts. You can either set `versions.drafts` to `true`, or pass an object to configure draft properties. -| Draft Option | Description | -| ---------------------------- | -------------| -| `autosave` | Enable `autosave` to automatically save progress while documents are edited. To enable, set to `true` or pass an object with [options](/docs/versions/autosave). | +| Draft Option | Description | +| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `autosave` | Enable `autosave` to automatically save progress while documents are edited. To enable, set to `true` or pass an object with [options](/docs/versions/autosave). | ### Database changes @@ -33,14 +31,18 @@ By enabling drafts on a collection or a global, Payload will automatical Within the Admin UI, if drafts are enabled, a document can be shown with one of three "statuses": -1. Draft - if a document has never been published, and only draft versions of the document are present +1. Draft - if a document has never been published, and only draft versions of the document + are present 1. Published - if a document is published and there are no newer drafts available -1. Changed - if a document has been published, but there are newer drafts available and not yet published +1. Changed - if a document has been published, but there are newer drafts available + and not yet published ### Draft API - If drafts are enabled on your collection or global, important and powerful changes are made to your REST, GraphQL, and Local APIs that allow you to specify if you are interacting with drafts or with live documents. + If drafts are enabled on your collection or global, important and powerful changes are made to + your REST, GraphQL, and Local APIs that allow you to specify if you are interacting with drafts or + with live documents. ##### Updating or creating drafts @@ -72,7 +74,9 @@ But, if you specify `draft` as `true`, Payload will automatically replace your p ### Controlling who can see Collection drafts - If you're using the drafts feature, it's important for you to consider who can view your drafts, and who can view only published documents. Luckily, Payload makes this extremely simple and puts the power completely in your hands. + If you're using the drafts feature, it's important for you to consider who can + view your drafts, and who can view only published documents. Luckily, Payload makes this extremely + simple and puts the power completely in your hands. ##### Restricting draft access @@ -82,7 +86,7 @@ You can use the `read` [Access Control](/docs/access-control/collections#read) m Here is an example that utilizes the `_status` field to require a user to be logged in to retrieve drafts: ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const Pages: CollectionConfig = { slug: 'pages', @@ -90,7 +94,7 @@ export const Pages: CollectionConfig = { read: ({ req }) => { // If there is a user logged in, // let them retrieve all documents - if (req.user) return true; + if (req.user) return true // If there is no user, // restrict the documents that are returned @@ -99,25 +103,32 @@ export const Pages: CollectionConfig = { _status: { equals: 'published', }, - }; + } }, }, versions: { - drafts: true + drafts: true, }, //.. the rest of the Pages config here } ``` - Note regarding adding versions to an existing collection
- If you already have a collection with documents, and you opt in to draft functionality after you have already created existing documents, all of your old documents will not have a _status field until you resave them. For this reason, if you are adding versions into an existing collection, you might want to write your access control function to allow for users to read both documents where _status is equal to "published" as well as where _status does not exist. + Note regarding adding versions to an existing collection +
+ If you already have a collection with documents, and you opt in to draft functionality + after you have already created existing documents, all of your old documents{' '} + will not have a _status field until you resave them. For this reason, if you are{' '} + adding versions into an existing collection, you might want to write your access control + function to allow for users to read both documents where{' '} + _status is equal to "published" as well as where{' '} + _status does not exist.
Here is an example for how to write an access control function that grants access to both documents where `_status` is equal to "published" and where `_status` does not exist: ```ts -import { CollectionConfig } from 'payload/types'; +import { CollectionConfig } from 'payload/types' export const Pages: CollectionConfig = { slug: 'pages', @@ -125,7 +136,7 @@ export const Pages: CollectionConfig = { read: ({ req }) => { // If there is a user logged in, // let them retrieve all documents - if (req.user) return true; + if (req.user) return true // If there is no user, // restrict the documents that are returned @@ -141,14 +152,14 @@ export const Pages: CollectionConfig = { { _status: { exists: false, - } - } - ] - }; + }, + }, + ], + } }, }, versions: { - drafts: true + drafts: true, }, //.. the rest of the Pages config here } @@ -161,4 +172,3 @@ If a document is published, the Payload Admin UI will be updated to show an "unp ### Reverting to published If a document is published, and you have made further changes which are saved as a draft, Payload will show a "revert to published" button at the top of the sidebar which will allow you to reject your draft changes and "revert" back to the published state of the document. Your drafts will still be saved, but a new version will be created that will reflect the last published state of the document. - diff --git a/docs/versions/overview.mdx b/docs/versions/overview.mdx index af67c7745..0b00fafea 100644 --- a/docs/versions/overview.mdx +++ b/docs/versions/overview.mdx @@ -7,8 +7,8 @@ keywords: version history, revisions, audit log, draft, publish, restore, autosa --- - Payload's powerful Versions functionality allows you to keep a running history - of changes over time and extensible to fit any content publishing workflow. + Payload's powerful Versions functionality allows you to keep a running history of changes over + time and extensible to fit any content publishing workflow. When enabled, Payload will automatically scaffold a new Collection in your database to store versions of your document(s) over time, and the Admin UI will be extended with additional views that allow you to browse document versions, view diffs in order to see exactly what has changed in your documents (and when they changed), and restore documents back to prior versions easily. @@ -26,9 +26,9 @@ _Comparing an old version to a newer version of a document_ - Build a powerful publishing schedule mechanism to create documents and have them become publicly readable automatically at a future date - Versions are extremely performant and totally opt-in. They don't change the - shape of your data at all. All versions are stored in a separate Collection - and can be turned on and off easily at your discretion. + Versions are extremely performant and totally opt-in. They don't change the shape of your data at + all. All versions are stored in a separate Collection and can be turned on and off easily at your + discretion. ### Options @@ -126,18 +126,18 @@ Versions expose new operations for both collections and globals. They allow you // Result will be a paginated set of Versions. // See /docs/queries/pagination for more. const result = await payload.findVersions({ - collection: "posts", // required + collection: 'posts', // required depth: 2, page: 1, limit: 10, where: {}, // pass a `where` query here - sort: "-createdAt", - locale: "en", + sort: '-createdAt', + locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, -}); +}) ``` #### Find by ID @@ -145,15 +145,15 @@ const result = await payload.findVersions({ ```js // Result will be a Post document. const result = await payload.findVersionByID({ - collection: "posts", // required - id: "507f1f77bcf86cd799439013", // required + collection: 'posts', // required + id: '507f1f77bcf86cd799439013', // required depth: 2, - locale: "en", + locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, -}); +}) ``` #### Restore @@ -161,13 +161,13 @@ const result = await payload.findVersionByID({ ```js // Result will be the restored global document. const result = await payload.restoreVersion({ - collection: "posts", // required - id: "507f1f77bcf86cd799439013", // required + collection: 'posts', // required + id: '507f1f77bcf86cd799439013', // required depth: 2, user: dummyUser, overrideAccess: false, showHiddenFields: true, -}); +}) ``` **Global REST endpoints:** @@ -199,18 +199,18 @@ const result = await payload.restoreVersion({ // Result will be a paginated set of Versions. // See /docs/queries/pagination for more. const result = await payload.findGlobalVersions({ - slug: "header", // required + slug: 'header', // required depth: 2, page: 1, limit: 10, where: {}, // pass a `where` query here - sort: "-createdAt", - locale: "en", + sort: '-createdAt', + locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, -}); +}) ``` #### Find by ID @@ -218,15 +218,15 @@ const result = await payload.findGlobalVersions({ ```js // Result will be a Post document. const result = await payload.findGlobalVersionByID({ - slug: "header", // required - id: "507f1f77bcf86cd799439013", // required + slug: 'header', // required + id: '507f1f77bcf86cd799439013', // required depth: 2, - locale: "en", + locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, -}); +}) ``` #### Restore @@ -234,13 +234,13 @@ const result = await payload.findGlobalVersionByID({ ```js // Result will be the restored global document. const result = await payload.restoreGlobalVersion({ - slug: "header", // required - id: "507f1f77bcf86cd799439013", // required + slug: 'header', // required + id: '507f1f77bcf86cd799439013', // required depth: 2, user: dummyUser, overrideAccess: false, showHiddenFields: true, -}); +}) ``` ### Access Control diff --git a/package.json b/package.json index f84b43cff..f1f093670 100644 --- a/package.json +++ b/package.json @@ -1,70 +1,43 @@ { - "name": "payload-monorepo", - "private": true, - "version": "0.0.0", - "description": "Node, React and MongoDB Headless CMS and Application Framework", - "license": "MIT", - "engines": { - "node": ">=14", - "pnpm": ">=8" - }, "author": { "email": "info@payloadcms.com", "name": "Payload", "url": "https://payloadcms.com" }, - "maintainers": [ - { - "name": "Payload", - "email": "info@payloadcms.com", - "url": "https://payloadcms.com" - } - ], - "repository": { - "type": "git", - "url": "https://github.com/payloadcms/payload.git" - }, - "homepage": "https://payloadcms.com", - "main": "./dist/index.js", - "typings": "./dist/index.d.ts", - "sideEffects": false, "bin": { "payload": "bin.js" }, - "workspaces:": [ - "packages/*" - ], - "scripts": { - "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/", - "build:tsc": "tsc", - "build:components": "webpack --config dist/bundlers/webpack/components.config.js", - "build": "pnpm copyfiles && pnpm build:tsc && pnpm build:components", - "build:watch": "nodemon --watch 'src/**' --ext 'ts,tsx' --exec \"pnpm build:tsc\"", - "dev": "pnpm --filter payload run dev", - "dev:postgres": "pnpm --filter payload run dev:postgres", - "dev:generate-types": "pnpm --filter payload run dev:generate-types", - "dev:generate-graphql-schema": "pnpm --filter payload run dev:generate-graphql-schema", - "pretest": "pnpm build", - "test": "pnpm --filter payload run test", - "test:int": "pnpm --filter payload run test:int", - "test:e2e": "pnpm --filter payload run test:e2e", - "test:e2e:headed": "pnpm --filter payload run test:e2e:headed", - "test:e2e:debug": "pnpm --filter payload run test:e2e:debug", - "test:components": "pnpm --filter payload run test:components", - "translateNewKeys": "pnpm --filter payload run translateNewKeys", - "clean:cache": "rimraf node_modules/.cache && rimraf packages/payload/node_modules/.cache", - "clean": "rimraf dist && rimraf packages/payload/dist", - "release:patch": "release-it patch", - "release:minor": "release-it minor", - "release:major": "release-it major", - "release:beta": "release-it pre --preReleaseId=beta --npm.tag=beta --config .release-it.pre.json", - "release:canary": "release-it pre --preReleaseId=canary --npm.tag=canary --config .release-it.pre.json", - "fix": "eslint \"src/**/*.ts\" --fix", - "lint": "eslint \"src/**/*.ts\"" - }, "bugs": { "url": "https://github.com/payloadcms/payload" }, + "dependencies": { + "turbo": "^1.10.13" + }, + "description": "Node, React and MongoDB Headless CMS and Application Framework", + "devDependencies": { + "@payloadcms/eslint-config": "workspace:*", + "@types/node": "20.5.7", + "copyfiles": "2.4.1", + "cross-env": "7.0.3", + "prettier": "^3.0.3", + "typescript": "5.2.2" + }, + "engines": { + "node": ">=14", + "pnpm": ">=8" + }, + "files": [ + "bin.js", + "dist", + "docs", + "components", + "scss", + "*.js", + "*.d.ts", + "!jest.config.js", + "!jest.components.config.js" + ], + "homepage": "https://payloadcms.com", "keywords": [ "payload", "cms", @@ -86,29 +59,56 @@ "react", "auth" ], - "dependencies": { - "turbo": "^1.10.13" - }, - "devDependencies": { - "@payloadcms/eslint-config": "workspace:*", - "@types/node": "20.5.7", - "copyfiles": "2.4.1", - "cross-env": "7.0.3", - "prettier": "^3.0.3", - "typescript": "5.2.2" - }, - "files": [ - "bin.js", - "dist", - "docs", - "components", - "scss", - "*.js", - "*.d.ts", - "!jest.config.js", - "!jest.components.config.js" + "license": "MIT", + "main": "./dist/index.js", + "maintainers": [ + { + "email": "info@payloadcms.com", + "name": "Payload", + "url": "https://payloadcms.com" + } ], + "name": "payload-monorepo", + "private": true, "publishConfig": { "registry": "https://registry.npmjs.org/" - } + }, + "repository": { + "type": "git", + "url": "https://github.com/payloadcms/payload.git" + }, + "scripts": { + "build": "pnpm copyfiles && pnpm build:tsc && pnpm build:components", + "build:components": "webpack --config dist/bundlers/webpack/components.config.js", + "build:tsc": "tsc", + "build:watch": "nodemon --watch 'src/**' --ext 'ts,tsx' --exec \"pnpm build:tsc\"", + "clean": "rimraf dist && rimraf packages/payload/dist", + "clean:cache": "rimraf node_modules/.cache && rimraf packages/payload/node_modules/.cache", + "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/", + "dev": "pnpm --filter payload run dev", + "dev:generate-graphql-schema": "pnpm --filter payload run dev:generate-graphql-schema", + "dev:generate-types": "pnpm --filter payload run dev:generate-types", + "dev:postgres": "pnpm --filter payload run dev:postgres", + "fix": "eslint \"src/**/*.ts\" --fix", + "lint": "eslint \"src/**/*.ts\"", + "pretest": "pnpm build", + "release:beta": "release-it pre --preReleaseId=beta --npm.tag=beta --config .release-it.pre.json", + "release:canary": "release-it pre --preReleaseId=canary --npm.tag=canary --config .release-it.pre.json", + "release:major": "release-it major", + "release:minor": "release-it minor", + "release:patch": "release-it patch", + "test": "pnpm --filter payload run test", + "test:components": "pnpm --filter payload run test:components", + "test:e2e": "pnpm --filter payload run test:e2e", + "test:e2e:debug": "pnpm --filter payload run test:e2e:debug", + "test:e2e:headed": "pnpm --filter payload run test:e2e:headed", + "test:int": "pnpm --filter payload run test:int", + "translateNewKeys": "pnpm --filter payload run translateNewKeys" + }, + "sideEffects": false, + "typings": "./dist/index.d.ts", + "version": "0.0.0", + "workspaces:": [ + "packages/*" + ] } diff --git a/packages/db-mongodb/.eslintrc.cjs b/packages/db-mongodb/.eslintrc.cjs index 6a2f2ade0..638d7f813 100644 --- a/packages/db-mongodb/.eslintrc.cjs +++ b/packages/db-mongodb/.eslintrc.cjs @@ -4,7 +4,7 @@ module.exports = { overrides: [ { extends: ['plugin:@typescript-eslint/disable-type-checked'], - files: ['*.js', '*.cjs'], + files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'], }, ], parserOptions: { diff --git a/packages/db-mongodb/package.json b/packages/db-mongodb/package.json index 816a54c1e..ee738b15a 100644 --- a/packages/db-mongodb/package.json +++ b/packages/db-mongodb/package.json @@ -1,38 +1,5 @@ { - "name": "@payloadcms/db-mongodb", - "version": "0.0.1", - "description": "The officially supported MongoDB database adapter for Payload", - "main": "./src/index.ts", - "types": "./src/index.ts", - "publishConfig": { - "registry": "https://registry.npmjs.org/", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.js", - "default": "./dist/index.js" - } - } - }, - "exports": { - ".": { - "types": "./src/index.ts", - "default": "./src/index.ts" - } - }, - "repository": "https://github.com/payloadcms/payload", "author": "Payload CMS, Inc.", - "license": "MIT", - "scripts": { - "build": "tsc" - }, - "devDependencies": { - "@types/mongoose-aggregate-paginate-v2": "1.0.9", - "mongodb-memory-server": "8.13.0", - "payload": "workspace:*", - "@payloadcms/eslint-config": "workspace:*" - }, "dependencies": { "bson-objectid": "2.0.4", "deepmerge": "4.3.1", @@ -41,5 +8,38 @@ "mongoose-aggregate-paginate-v2": "1.0.6", "mongoose-paginate-v2": "1.7.22", "uuid": "9.0.0" - } + }, + "description": "The officially supported MongoDB database adapter for Payload", + "devDependencies": { + "@payloadcms/eslint-config": "workspace:*", + "@types/mongoose-aggregate-paginate-v2": "1.0.9", + "mongodb-memory-server": "8.13.0", + "payload": "workspace:*" + }, + "exports": { + ".": { + "default": "./src/index.ts", + "types": "./src/index.ts" + } + }, + "license": "MIT", + "main": "./src/index.ts", + "name": "@payloadcms/db-mongodb", + "publishConfig": { + "exports": { + ".": { + "default": "./dist/index.js", + "types": "./dist/index.js" + } + }, + "main": "./dist/index.js", + "registry": "https://registry.npmjs.org/", + "types": "./dist/index.d.ts" + }, + "repository": "https://github.com/payloadcms/payload", + "scripts": { + "build": "tsc" + }, + "types": "./src/index.ts", + "version": "0.0.1" } diff --git a/packages/db-mongodb/src/connect.ts b/packages/db-mongodb/src/connect.ts index c0d04c620..7ec257952 100644 --- a/packages/db-mongodb/src/connect.ts +++ b/packages/db-mongodb/src/connect.ts @@ -1,67 +1,61 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -import type { ConnectOptions } from 'mongoose'; -import mongoose from 'mongoose'; -import type { Connect } from 'payload/database'; -import type { MongooseAdapter } from '.'; +import type { ConnectOptions } from 'mongoose' +import type { Connect } from 'payload/database' -export const connect: Connect = async function connect( - this: MongooseAdapter, - payload, -) { +import mongoose from 'mongoose' + +import type { MongooseAdapter } from '.' + +export const connect: Connect = async function connect(this: MongooseAdapter, payload) { if (this.url === false) { - return; + return } if (!payload.local && typeof this.url !== 'string') { - throw new Error('Error: missing MongoDB connection URL.'); + throw new Error('Error: missing MongoDB connection URL.') } - let urlToConnect = this.url; - let successfulConnectionMessage = 'Connected to MongoDB server successfully!'; + let urlToConnect = this.url + let successfulConnectionMessage = 'Connected to MongoDB server successfully!' const connectionOptions: ConnectOptions & { useFacet: undefined } = { autoIndex: true, ...this.connectOptions, useFacet: undefined, - }; + } if (process.env.NODE_ENV === 'test') { if (process.env.PAYLOAD_TEST_MONGO_URL) { - urlToConnect = process.env.PAYLOAD_TEST_MONGO_URL; + urlToConnect = process.env.PAYLOAD_TEST_MONGO_URL } else { - connectionOptions.dbName = 'payloadmemory'; - const { MongoMemoryServer } = require('mongodb-memory-server'); - const getPort = require('get-port'); + connectionOptions.dbName = 'payloadmemory' + const { MongoMemoryServer } = require('mongodb-memory-server') + const getPort = require('get-port') - const port = await getPort(); + const port = await getPort() this.mongoMemoryServer = await MongoMemoryServer.create({ instance: { dbName: 'payloadmemory', port, }, - }); + }) - urlToConnect = this.mongoMemoryServer.getUri(); - successfulConnectionMessage = 'Connected to in-memory MongoDB server successfully!'; + urlToConnect = this.mongoMemoryServer.getUri() + successfulConnectionMessage = 'Connected to in-memory MongoDB server successfully!' } } try { - this.connection = ( - await mongoose.connect(urlToConnect, connectionOptions) - ).connection; + this.connection = (await mongoose.connect(urlToConnect, connectionOptions)).connection if (process.env.PAYLOAD_DROP_DATABASE === 'true') { - this.payload.logger.info('---- DROPPING DATABASE ----'); - await mongoose.connection.dropDatabase(); - this.payload.logger.info('---- DROPPED DATABASE ----'); + this.payload.logger.info('---- DROPPING DATABASE ----') + await mongoose.connection.dropDatabase() + this.payload.logger.info('---- DROPPED DATABASE ----') } - this.payload.logger.info(successfulConnectionMessage); + this.payload.logger.info(successfulConnectionMessage) } catch (err) { - this.payload.logger.error( - `Error: cannot connect to MongoDB. Details: ${err.message}`, - err, - ); - process.exit(1); + this.payload.logger.error(`Error: cannot connect to MongoDB. Details: ${err.message}`, err) + process.exit(1) } -}; +} diff --git a/packages/db-mongodb/src/create.ts b/packages/db-mongodb/src/create.ts index 583b8b3ec..ba7133c21 100644 --- a/packages/db-mongodb/src/create.ts +++ b/packages/db-mongodb/src/create.ts @@ -1,27 +1,29 @@ -import { Create } from 'payload/database'; -import type { Document } from 'payload/types'; -import { PayloadRequest } from 'payload/types'; -import type { MongooseAdapter } from '.'; -import { withSession } from './withSession'; +import type { Create } from 'payload/database' +import type { PayloadRequest } from 'payload/types' +import type { Document } from 'payload/types' + +import type { MongooseAdapter } from '.' + +import { withSession } from './withSession' export const create: Create = async function create( this: MongooseAdapter, { collection, data, req = {} as PayloadRequest }, ) { - const Model = this.collections[collection]; - const options = withSession(this, req.transactionID); + const Model = this.collections[collection] + const options = withSession(this, req.transactionID) - const [doc] = await Model.create([data], options); + const [doc] = await Model.create([data], options) // doc.toJSON does not do stuff like converting ObjectIds to string, or date strings to date objects. That's why we use JSON.parse/stringify here - const result: Document = JSON.parse(JSON.stringify(doc)); - const verificationToken = doc._verificationToken; + const result: Document = JSON.parse(JSON.stringify(doc)) + const verificationToken = doc._verificationToken // custom id type reset - result.id = result._id; + result.id = result._id if (verificationToken) { - result._verificationToken = verificationToken; + result._verificationToken = verificationToken } - return result; -}; + return result +} diff --git a/packages/db-mongodb/src/createGlobal.ts b/packages/db-mongodb/src/createGlobal.ts index 42732426c..828e369fd 100644 --- a/packages/db-mongodb/src/createGlobal.ts +++ b/packages/db-mongodb/src/createGlobal.ts @@ -1,27 +1,29 @@ -import { PayloadRequest } from 'payload/types'; -import { CreateGlobal } from 'payload/database'; -import sanitizeInternalFields from './utilities/sanitizeInternalFields'; -import { withSession } from './withSession'; -import type { MongooseAdapter } from '.'; +import type { CreateGlobal } from 'payload/database' +import type { PayloadRequest } from 'payload/types' + +import type { MongooseAdapter } from '.' + +import sanitizeInternalFields from './utilities/sanitizeInternalFields' +import { withSession } from './withSession' export const createGlobal: CreateGlobal = async function createGlobal( this: MongooseAdapter, - { data, slug, req = {} as PayloadRequest }, + { data, req = {} as PayloadRequest, slug }, ) { - const Model = this.globals; + const Model = this.globals const global = { globalType: slug, ...data, - }; - const options = withSession(this, req.transactionID); + } + const options = withSession(this, req.transactionID) - let [result] = (await Model.create([global], options)) as any; + let [result] = (await Model.create([global], options)) as any - result = JSON.parse(JSON.stringify(result)); + result = JSON.parse(JSON.stringify(result)) // custom id type reset - result.id = result._id; - result = sanitizeInternalFields(result); + result.id = result._id + result = sanitizeInternalFields(result) - return result; -}; + return result +} diff --git a/packages/db-mongodb/src/createVersion.ts b/packages/db-mongodb/src/createVersion.ts index 174247c16..f309e6edd 100644 --- a/packages/db-mongodb/src/createVersion.ts +++ b/packages/db-mongodb/src/createVersion.ts @@ -1,45 +1,47 @@ -import type { CreateVersion } from 'payload/database'; -import { PayloadRequest } from 'payload/types'; -import type { Document } from 'payload/types'; -import type { MongooseAdapter } from '.'; -import { withSession } from './withSession'; +import type { CreateVersion } from 'payload/database' +import type { PayloadRequest } from 'payload/types' +import type { Document } from 'payload/types' + +import type { MongooseAdapter } from '.' + +import { withSession } from './withSession' export const createVersion: CreateVersion = async function createVersion( this: MongooseAdapter, { - collectionSlug, - parent, - versionData, autosave, + collectionSlug, createdAt, - updatedAt, + parent, req = {} as PayloadRequest, + updatedAt, + versionData, }, ) { - const VersionModel = this.versions[collectionSlug]; - const options = withSession(this, req.transactionID); + const VersionModel = this.versions[collectionSlug] + const options = withSession(this, req.transactionID) const [doc] = await VersionModel.create( [ { - parent, - version: versionData, autosave, createdAt, + parent, updatedAt, + version: versionData, }, ], options, req, - ); + ) - const result: Document = JSON.parse(JSON.stringify(doc)); - const verificationToken = doc._verificationToken; + const result: Document = JSON.parse(JSON.stringify(doc)) + const verificationToken = doc._verificationToken // custom id type reset - result.id = result._id; + result.id = result._id if (verificationToken) { - result._verificationToken = verificationToken; + result._verificationToken = verificationToken } - return result; -}; + return result +} diff --git a/packages/db-mongodb/src/deleteMany.ts b/packages/db-mongodb/src/deleteMany.ts index d4b4449a1..5d4104f65 100644 --- a/packages/db-mongodb/src/deleteMany.ts +++ b/packages/db-mongodb/src/deleteMany.ts @@ -1,20 +1,24 @@ -import { DeleteMany } from 'payload/database'; -import { PayloadRequest } from 'payload/types'; -import type { MongooseAdapter } from '.'; -import { withSession } from './withSession'; +import type { DeleteMany } from 'payload/database' +import type { PayloadRequest } from 'payload/types' -export const deleteMany: DeleteMany = async function deleteMany(this: MongooseAdapter, - { collection, where, req = {} as PayloadRequest }) { - const Model = this.collections[collection]; +import type { MongooseAdapter } from '.' + +import { withSession } from './withSession' + +export const deleteMany: DeleteMany = async function deleteMany( + this: MongooseAdapter, + { collection, req = {} as PayloadRequest, where }, +) { + const Model = this.collections[collection] const options = { ...withSession(this, req.transactionID), lean: true, - }; + } const query = await Model.buildQuery({ payload: this.payload, where, - }); + }) - await Model.deleteMany(query, options); -}; + await Model.deleteMany(query, options) +} diff --git a/packages/db-mongodb/src/deleteOne.ts b/packages/db-mongodb/src/deleteOne.ts index b6959e8cb..3bb4d8879 100644 --- a/packages/db-mongodb/src/deleteOne.ts +++ b/packages/db-mongodb/src/deleteOne.ts @@ -1,29 +1,31 @@ -import { DeleteOne } from 'payload/database'; -import type { Document } from 'payload/types'; -import { PayloadRequest } from 'payload/types'; -import sanitizeInternalFields from './utilities/sanitizeInternalFields'; -import type { MongooseAdapter } from '.'; -import { withSession } from './withSession'; +import type { DeleteOne } from 'payload/database' +import type { PayloadRequest } from 'payload/types' +import type { Document } from 'payload/types' + +import type { MongooseAdapter } from '.' + +import sanitizeInternalFields from './utilities/sanitizeInternalFields' +import { withSession } from './withSession' export const deleteOne: DeleteOne = async function deleteOne( this: MongooseAdapter, - { collection, where, req = {} as PayloadRequest }, + { collection, req = {} as PayloadRequest, where }, ) { - const Model = this.collections[collection]; - const options = withSession(this, req.transactionID); + const Model = this.collections[collection] + const options = withSession(this, req.transactionID) const query = await Model.buildQuery({ payload: this.payload, where, - }); + }) - const doc = await Model.findOneAndDelete(query, options).lean(); + const doc = await Model.findOneAndDelete(query, options).lean() - let result: Document = JSON.parse(JSON.stringify(doc)); + let result: Document = JSON.parse(JSON.stringify(doc)) // custom id type reset - result.id = result._id; - result = sanitizeInternalFields(result); + result.id = result._id + result = sanitizeInternalFields(result) - return result; -}; + return result +} diff --git a/packages/db-mongodb/src/deleteVersions.ts b/packages/db-mongodb/src/deleteVersions.ts index 4152ff07f..0f906139e 100644 --- a/packages/db-mongodb/src/deleteVersions.ts +++ b/packages/db-mongodb/src/deleteVersions.ts @@ -1,21 +1,25 @@ -import { DeleteVersions } from 'payload/database'; -import { PayloadRequest } from 'payload/types'; -import type { MongooseAdapter } from '.'; -import { withSession } from './withSession'; +import type { DeleteVersions } from 'payload/database' +import type { PayloadRequest } from 'payload/types' -export const deleteVersions: DeleteVersions = async function deleteVersions(this: MongooseAdapter, - { collection, where, locale, req = {} as PayloadRequest }) { - const VersionsModel = this.versions[collection]; +import type { MongooseAdapter } from '.' + +import { withSession } from './withSession' + +export const deleteVersions: DeleteVersions = async function deleteVersions( + this: MongooseAdapter, + { collection, locale, req = {} as PayloadRequest, where }, +) { + const VersionsModel = this.versions[collection] const options = { ...withSession(this, req.transactionID), lean: true, - }; + } const query = await VersionsModel.buildQuery({ - payload: this.payload, locale, + payload: this.payload, where, - }); + }) - await VersionsModel.deleteMany(query, options); -}; + await VersionsModel.deleteMany(query, options) +} diff --git a/packages/db-mongodb/src/destroy.ts b/packages/db-mongodb/src/destroy.ts index f9b400c06..8f0ebe3b5 100644 --- a/packages/db-mongodb/src/destroy.ts +++ b/packages/db-mongodb/src/destroy.ts @@ -1,13 +1,13 @@ -import mongoose from 'mongoose'; -import { Destroy } from 'payload/database'; -import { MongooseAdapter } from './index'; +import type { Destroy } from 'payload/database' -export const destroy: Destroy = async function destroy( - this: MongooseAdapter, -) { +import mongoose from 'mongoose' + +import type { MongooseAdapter } from './index' + +export const destroy: Destroy = async function destroy(this: MongooseAdapter) { if (this.mongoMemoryServer) { - await mongoose.connection.dropDatabase(); - await mongoose.connection.close(); - await this.mongoMemoryServer.stop(); + await mongoose.connection.dropDatabase() + await mongoose.connection.close() + await this.mongoMemoryServer.stop() } -}; +} diff --git a/packages/db-mongodb/src/find.ts b/packages/db-mongodb/src/find.ts index 1bc679fdb..a0440d25b 100644 --- a/packages/db-mongodb/src/find.ts +++ b/packages/db-mongodb/src/find.ts @@ -1,79 +1,73 @@ -import type { PaginateOptions } from 'mongoose'; -import type { Find } from 'payload/database'; -import { flattenWhereToOperators } from 'payload/database'; -import { PayloadRequest } from 'payload/types'; -import sanitizeInternalFields from './utilities/sanitizeInternalFields'; -import { buildSortParam } from './queries/buildSortParam'; -import type { MongooseAdapter } from '.'; -import { withSession } from './withSession'; +import type { PaginateOptions } from 'mongoose' +import type { Find } from 'payload/database' +import type { PayloadRequest } from 'payload/types' + +import { flattenWhereToOperators } from 'payload/database' + +import type { MongooseAdapter } from '.' + +import { buildSortParam } from './queries/buildSortParam' +import sanitizeInternalFields from './utilities/sanitizeInternalFields' +import { withSession } from './withSession' export const find: Find = async function find( this: MongooseAdapter, - { - collection, - where, - page, - limit, - sort: sortArg, - locale, - pagination, - req = {} as PayloadRequest, - }, + { collection, limit, locale, page, pagination, req = {} as PayloadRequest, sort: sortArg, where }, ) { - const Model = this.collections[collection]; - const collectionConfig = this.payload.collections[collection].config; - const options = withSession(this, req.transactionID); + const Model = this.collections[collection] + const collectionConfig = this.payload.collections[collection].config + const options = withSession(this, req.transactionID) - let hasNearConstraint = false; + let hasNearConstraint = false if (where) { - const constraints = flattenWhereToOperators(where); - hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')); + const constraints = flattenWhereToOperators(where) + hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) } - let sort; + let sort if (!hasNearConstraint) { sort = buildSortParam({ - sort: sortArg || collectionConfig.defaultSort, - fields: collectionConfig.fields, - timestamps: true, config: this.payload.config, + fields: collectionConfig.fields, locale, - }); + sort: sortArg || collectionConfig.defaultSort, + timestamps: true, + }) } const query = await Model.buildQuery({ - payload: this.payload, locale, + payload: this.payload, where, - }); + }) const paginationOptions: PaginateOptions = { - page, - sort, + forceCountFn: hasNearConstraint, lean: true, leanWithId: true, - useEstimatedCount: hasNearConstraint, - forceCountFn: hasNearConstraint, - pagination, options, - }; - - if (limit > 0) { - paginationOptions.limit = limit; - // limit must also be set here, it's ignored when pagination is false - paginationOptions.options.limit = limit; + page, + pagination, + sort, + useEstimatedCount: hasNearConstraint, } - const result = await Model.paginate(query, paginationOptions); - const docs = JSON.parse(JSON.stringify(result.docs)); + if (limit > 0) { + paginationOptions.limit = limit + // limit must also be set here, it's ignored when pagination is false + paginationOptions.options.limit = limit + } + + const result = await Model.paginate(query, paginationOptions) + const docs = JSON.parse(JSON.stringify(result.docs)) return { ...result, docs: docs.map((doc) => { // eslint-disable-next-line no-param-reassign - doc.id = doc._id; - return sanitizeInternalFields(doc); + doc.id = doc._id + return sanitizeInternalFields(doc) }), - }; -}; + } +} diff --git a/packages/db-mongodb/src/findGlobal.ts b/packages/db-mongodb/src/findGlobal.ts index 1f477cc65..4519c8e34 100644 --- a/packages/db-mongodb/src/findGlobal.ts +++ b/packages/db-mongodb/src/findGlobal.ts @@ -1,39 +1,42 @@ -import { combineQueries } from 'payload/database'; -import type { FindGlobal } from 'payload/database'; -import { PayloadRequest } from 'payload/types'; -import sanitizeInternalFields from './utilities/sanitizeInternalFields'; -import type { MongooseAdapter } from '.'; -import { withSession } from './withSession'; +import type { FindGlobal } from 'payload/database' +import type { PayloadRequest } from 'payload/types' + +import { combineQueries } from 'payload/database' + +import type { MongooseAdapter } from '.' + +import sanitizeInternalFields from './utilities/sanitizeInternalFields' +import { withSession } from './withSession' export const findGlobal: FindGlobal = async function findGlobal( this: MongooseAdapter, - { slug, locale, where, req = {} as PayloadRequest }, + { locale, req = {} as PayloadRequest, slug, where }, ) { - const Model = this.globals; + const Model = this.globals const options = { ...withSession(this, req.transactionID), lean: true, - }; + } const query = await Model.buildQuery({ - where: combineQueries({ globalType: { equals: slug } }, where), - payload: this.payload, - locale, globalSlug: slug, - }); + locale, + payload: this.payload, + where: combineQueries({ globalType: { equals: slug } }, where), + }) - let doc = (await Model.findOne(query, {}, options)) as any; + let doc = (await Model.findOne(query, {}, options)) as any if (!doc) { - return null; + return null } if (doc._id) { - doc.id = doc._id; - delete doc._id; + doc.id = doc._id + delete doc._id } - doc = JSON.parse(JSON.stringify(doc)); - doc = sanitizeInternalFields(doc); + doc = JSON.parse(JSON.stringify(doc)) + doc = sanitizeInternalFields(doc) - return doc; -}; + return doc +} diff --git a/packages/db-mongodb/src/findGlobalVersions.ts b/packages/db-mongodb/src/findGlobalVersions.ts index 758fe2a40..b915a7933 100644 --- a/packages/db-mongodb/src/findGlobalVersions.ts +++ b/packages/db-mongodb/src/findGlobalVersions.ts @@ -1,89 +1,92 @@ -import { PaginateOptions } from 'mongoose'; -import type { FindGlobalVersions } from 'payload/database'; -import { flattenWhereToOperators } from 'payload/database'; -import { buildVersionGlobalFields } from 'payload/versions'; -import { PayloadRequest } from 'payload/types'; -import sanitizeInternalFields from './utilities/sanitizeInternalFields'; -import type { MongooseAdapter } from '.'; -import { buildSortParam } from './queries/buildSortParam'; -import { withSession } from './withSession'; +import type { PaginateOptions } from 'mongoose' +import type { FindGlobalVersions } from 'payload/database' +import type { PayloadRequest } from 'payload/types' + +import { flattenWhereToOperators } from 'payload/database' +import { buildVersionGlobalFields } from 'payload/versions' + +import type { MongooseAdapter } from '.' + +import { buildSortParam } from './queries/buildSortParam' +import sanitizeInternalFields from './utilities/sanitizeInternalFields' +import { withSession } from './withSession' export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions( this: MongooseAdapter, { global, - where, - page, limit, - sort: sortArg, locale, + page, pagination, - skip, req = {} as PayloadRequest, + skip, + sort: sortArg, + where, }, ) { - const Model = this.versions[global]; + const Model = this.versions[global] const versionFields = buildVersionGlobalFields( this.payload.globals.config.find(({ slug }) => slug === global), - ); + ) const options = { ...withSession(this, req.transactionID), - skip, limit, - }; - - let hasNearConstraint = false; - - if (where) { - const constraints = flattenWhereToOperators(where); - hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')); + skip, } - let sort; + let hasNearConstraint = false + + if (where) { + const constraints = flattenWhereToOperators(where) + hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) + } + + let sort if (!hasNearConstraint) { sort = buildSortParam({ - sort: sortArg || '-updatedAt', - fields: versionFields, - timestamps: true, config: this.payload.config, + fields: versionFields, locale, - }); + sort: sortArg || '-updatedAt', + timestamps: true, + }) } const query = await Model.buildQuery({ - payload: this.payload, - locale, - where, globalSlug: global, - }); + locale, + payload: this.payload, + where, + }) const paginationOptions: PaginateOptions = { - page, - sort, + forceCountFn: hasNearConstraint, lean: true, leanWithId: true, - pagination, offset: skip, - useEstimatedCount: hasNearConstraint, - forceCountFn: hasNearConstraint, options, - }; - - if (limit > 0) { - paginationOptions.limit = limit; - // limit must also be set here, it's ignored when pagination is false - paginationOptions.options.limit = limit; + page, + pagination, + sort, + useEstimatedCount: hasNearConstraint, } - const result = await Model.paginate(query, paginationOptions); - const docs = JSON.parse(JSON.stringify(result.docs)); + if (limit > 0) { + paginationOptions.limit = limit + // limit must also be set here, it's ignored when pagination is false + paginationOptions.options.limit = limit + } + + const result = await Model.paginate(query, paginationOptions) + const docs = JSON.parse(JSON.stringify(result.docs)) return { ...result, docs: docs.map((doc) => { // eslint-disable-next-line no-param-reassign - doc.id = doc._id; - return sanitizeInternalFields(doc); + doc.id = doc._id + return sanitizeInternalFields(doc) }), - }; -}; + } +} diff --git a/packages/db-mongodb/src/findOne.ts b/packages/db-mongodb/src/findOne.ts index 9d785020a..7d8f9bbe2 100644 --- a/packages/db-mongodb/src/findOne.ts +++ b/packages/db-mongodb/src/findOne.ts @@ -1,38 +1,40 @@ -import type { MongooseQueryOptions } from 'mongoose'; -import type { FindOne } from 'payload/database'; -import type { Document } from 'payload/types'; -import { PayloadRequest } from 'payload/types'; -import sanitizeInternalFields from './utilities/sanitizeInternalFields'; -import type { MongooseAdapter } from '.'; -import { withSession } from './withSession'; +import type { MongooseQueryOptions } from 'mongoose' +import type { FindOne } from 'payload/database' +import type { PayloadRequest } from 'payload/types' +import type { Document } from 'payload/types' + +import type { MongooseAdapter } from '.' + +import sanitizeInternalFields from './utilities/sanitizeInternalFields' +import { withSession } from './withSession' export const findOne: FindOne = async function findOne( this: MongooseAdapter, - { collection, where, locale, req = {} as PayloadRequest }, + { collection, locale, req = {} as PayloadRequest, where }, ) { - const Model = this.collections[collection]; + const Model = this.collections[collection] const options: MongooseQueryOptions = { ...withSession(this, req.transactionID), lean: true, - }; - - const query = await Model.buildQuery({ - payload: this.payload, - locale, - where, - }); - - const doc = await Model.findOne(query, {}, options); - - if (!doc) { - return null; } - let result: Document = JSON.parse(JSON.stringify(doc)); + const query = await Model.buildQuery({ + locale, + payload: this.payload, + where, + }) + + const doc = await Model.findOne(query, {}, options) + + if (!doc) { + return null + } + + let result: Document = JSON.parse(JSON.stringify(doc)) // custom id type reset - result.id = result._id; - result = sanitizeInternalFields(result); + result.id = result._id + result = sanitizeInternalFields(result) - return result; -}; + return result +} diff --git a/packages/db-mongodb/src/findVersions.ts b/packages/db-mongodb/src/findVersions.ts index ce6c8717a..f8b86cec8 100644 --- a/packages/db-mongodb/src/findVersions.ts +++ b/packages/db-mongodb/src/findVersions.ts @@ -1,86 +1,89 @@ -import { PaginateOptions } from 'mongoose'; -import type { FindVersions } from 'payload/database'; -import { flattenWhereToOperators } from 'payload/database'; -import { PayloadRequest } from 'payload/types'; -import sanitizeInternalFields from './utilities/sanitizeInternalFields'; -import type { MongooseAdapter } from '.'; -import { buildSortParam } from './queries/buildSortParam'; -import { withSession } from './withSession'; +import type { PaginateOptions } from 'mongoose' +import type { FindVersions } from 'payload/database' +import type { PayloadRequest } from 'payload/types' + +import { flattenWhereToOperators } from 'payload/database' + +import type { MongooseAdapter } from '.' + +import { buildSortParam } from './queries/buildSortParam' +import sanitizeInternalFields from './utilities/sanitizeInternalFields' +import { withSession } from './withSession' export const findVersions: FindVersions = async function findVersions( this: MongooseAdapter, { collection, - where, - page, limit, - sort: sortArg, locale, + page, pagination, - skip, req = {} as PayloadRequest, + skip, + sort: sortArg, + where, }, ) { - const Model = this.versions[collection]; - const collectionConfig = this.payload.collections[collection].config; + const Model = this.versions[collection] + const collectionConfig = this.payload.collections[collection].config const options = { ...withSession(this, req.transactionID), - skip, limit, - }; - - let hasNearConstraint = false; - - if (where) { - const constraints = flattenWhereToOperators(where); - hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')); + skip, } - let sort; + let hasNearConstraint = false + + if (where) { + const constraints = flattenWhereToOperators(where) + hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) + } + + let sort if (!hasNearConstraint) { sort = buildSortParam({ - sort: sortArg || '-updatedAt', - fields: collectionConfig.fields, - timestamps: true, config: this.payload.config, + fields: collectionConfig.fields, locale, - }); + sort: sortArg || '-updatedAt', + timestamps: true, + }) } const query = await Model.buildQuery({ - payload: this.payload, locale, + payload: this.payload, where, - }); + }) const paginationOptions: PaginateOptions = { - page, - sort, - limit, + forceCountFn: hasNearConstraint, lean: true, leanWithId: true, - pagination, + limit, offset: skip, - useEstimatedCount: hasNearConstraint, - forceCountFn: hasNearConstraint, options, - }; - - if (limit > 0) { - paginationOptions.limit = limit; - // limit must also be set here, it's ignored when pagination is false - paginationOptions.options.limit = limit; + page, + pagination, + sort, + useEstimatedCount: hasNearConstraint, } - const result = await Model.paginate(query, paginationOptions); - const docs = JSON.parse(JSON.stringify(result.docs)); + if (limit > 0) { + paginationOptions.limit = limit + // limit must also be set here, it's ignored when pagination is false + paginationOptions.options.limit = limit + } + + const result = await Model.paginate(query, paginationOptions) + const docs = JSON.parse(JSON.stringify(result.docs)) return { ...result, docs: docs.map((doc) => { // eslint-disable-next-line no-param-reassign - doc.id = doc._id; - return sanitizeInternalFields(doc); + doc.id = doc._id + return sanitizeInternalFields(doc) }), - }; -}; + } +} diff --git a/packages/db-mongodb/src/index.ts b/packages/db-mongodb/src/index.ts index 9d655e4e7..7cdf0539d 100644 --- a/packages/db-mongodb/src/index.ts +++ b/packages/db-mongodb/src/index.ts @@ -1,108 +1,111 @@ -import type { ClientSession, Connection, ConnectOptions } from 'mongoose'; -import mongoose from 'mongoose'; -import { createMigration } from 'payload/database'; -import type { Payload } from 'payload'; -import type { DatabaseAdapter } from 'payload/database'; -import { createDatabaseAdapter } from 'payload/database'; -import { connect } from './connect'; -import { init } from './init'; -import { webpack } from './webpack'; -import { createGlobal } from './createGlobal'; -import { createVersion } from './createVersion'; -import { beginTransaction } from './transactions/beginTransaction'; -import { rollbackTransaction } from './transactions/rollbackTransaction'; -import { commitTransaction } from './transactions/commitTransaction'; -import { queryDrafts } from './queryDrafts'; -import { find } from './find'; -import { findGlobalVersions } from './findGlobalVersions'; -import { findVersions } from './findVersions'; -import { create } from './create'; -import { deleteOne } from './deleteOne'; -import { deleteVersions } from './deleteVersions'; -import { findGlobal } from './findGlobal'; -import { findOne } from './findOne'; -import { updateGlobal } from './updateGlobal'; -import { updateOne } from './updateOne'; -import { updateVersion } from './updateVersion'; -import { deleteMany } from './deleteMany'; -import { destroy } from './destroy'; -import type { CollectionModel, GlobalModel } from './types'; +import type { ClientSession, ConnectOptions, Connection } from 'mongoose' +import type { Payload } from 'payload' +import type { DatabaseAdapter } from 'payload/database' + +import mongoose from 'mongoose' +import { createDatabaseAdapter } from 'payload/database' +import { createMigration } from 'payload/database' + +import type { CollectionModel, GlobalModel } from './types' + +import { connect } from './connect' +import { create } from './create' +import { createGlobal } from './createGlobal' +import { createVersion } from './createVersion' +import { deleteMany } from './deleteMany' +import { deleteOne } from './deleteOne' +import { deleteVersions } from './deleteVersions' +import { destroy } from './destroy' +import { find } from './find' +import { findGlobal } from './findGlobal' +import { findGlobalVersions } from './findGlobalVersions' +import { findOne } from './findOne' +import { findVersions } from './findVersions' +import { init } from './init' +import { queryDrafts } from './queryDrafts' +import { beginTransaction } from './transactions/beginTransaction' +import { commitTransaction } from './transactions/commitTransaction' +import { rollbackTransaction } from './transactions/rollbackTransaction' +import { updateGlobal } from './updateGlobal' +import { updateOne } from './updateOne' +import { updateVersion } from './updateVersion' +import { webpack } from './webpack' export interface Args { - /** The URL to connect to MongoDB or false to start payload and prevent connecting */ - url: string | false; - migrationDir?: string; + /** Set to false to disable auto-pluralization of collection names, Defaults to true */ + autoPluralization?: boolean /** Extra configuration options */ connectOptions?: ConnectOptions & { /** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */ - useFacet?: boolean; - }; - /** Set to false to disable auto-pluralization of collection names, Defaults to true */ - autoPluralization?: boolean; + useFacet?: boolean + } + migrationDir?: string + /** The URL to connect to MongoDB or false to start payload and prevent connecting */ + url: false | string } export type MongooseAdapter = DatabaseAdapter & Args & { - mongoMemoryServer: any; collections: { - [slug: string]: CollectionModel; - }; - globals: GlobalModel; + [slug: string]: CollectionModel + } + connection: Connection + globals: GlobalModel + mongoMemoryServer: any + sessions: Record versions: { [slug: string]: CollectionModel } - sessions: Record - connection: Connection } type MongooseAdapterResult = (args: { payload: Payload }) => MongooseAdapter export function mongooseAdapter({ - url, + autoPluralization = true, connectOptions, migrationDir, - autoPluralization = true, + url, }: Args): MongooseAdapterResult { function adapter({ payload }: { payload: Payload }) { - mongoose.set('strictQuery', false); + mongoose.set('strictQuery', false) return createDatabaseAdapter({ - payload, - migrationDir, - connection: undefined, - mongoMemoryServer: undefined, - sessions: {}, - url, - connectOptions: connectOptions || {}, autoPluralization, - globals: undefined, - collections: {}, - versions: {}, - connect, - destroy, - init, - webpack, - createMigration, beginTransaction, - rollbackTransaction, + collections: {}, commitTransaction, - queryDrafts, - findOne, - find, + connect, + connectOptions: connectOptions || {}, + connection: undefined, create, - updateOne, - deleteOne, - deleteMany, - findGlobal, createGlobal, - updateGlobal, - findVersions, - findGlobalVersions, + createMigration, createVersion, - updateVersion, + deleteMany, + deleteOne, deleteVersions, - }); + destroy, + find, + findGlobal, + findGlobalVersions, + findOne, + findVersions, + globals: undefined, + init, + migrationDir, + mongoMemoryServer: undefined, + payload, + queryDrafts, + rollbackTransaction, + sessions: {}, + updateGlobal, + updateOne, + updateVersion, + url, + versions: {}, + webpack, + }) } - return adapter; + return adapter } diff --git a/packages/db-mongodb/src/init.ts b/packages/db-mongodb/src/init.ts index 47371f282..86a1fc519 100644 --- a/packages/db-mongodb/src/init.ts +++ b/packages/db-mongodb/src/init.ts @@ -1,129 +1,119 @@ /* eslint-disable no-param-reassign */ -import mongoose, { PaginateOptions } from 'mongoose'; -import paginate from 'mongoose-paginate-v2'; -import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'; -import { buildVersionCollectionFields } from 'payload/versions'; -import { SanitizedCollectionConfig } from 'payload/types'; -import { getVersionsModelName } from 'payload/versions'; -import { buildVersionGlobalFields } from 'payload/versions'; -import type { Init } from 'payload/database'; -import getBuildQueryPlugin from './queries/buildQuery'; -import buildCollectionSchema from './models/buildCollectionSchema'; -import buildSchema from './models/buildSchema'; -import type { MongooseAdapter } from '.'; -import { buildGlobalModel } from './models/buildGlobalModel'; -import { CollectionModel } from './types'; +import type { PaginateOptions } from 'mongoose' +import type { Init } from 'payload/database' +import type { SanitizedCollectionConfig } from 'payload/types' -export const init: Init = async function init( - this: MongooseAdapter, -) { - this.payload.config.collections.forEach( - (collection: SanitizedCollectionConfig) => { - const schema = buildCollectionSchema(collection, this.payload.config); +import mongoose from 'mongoose' +import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2' +import paginate from 'mongoose-paginate-v2' +import { buildVersionGlobalFields } from 'payload/versions' +import { buildVersionCollectionFields } from 'payload/versions' +import { getVersionsModelName } from 'payload/versions' - if (collection.versions) { - const versionModelName = getVersionsModelName(collection); +import type { MongooseAdapter } from '.' +import type { CollectionModel } from './types' - const versionCollectionFields = buildVersionCollectionFields(collection); +import buildCollectionSchema from './models/buildCollectionSchema' +import { buildGlobalModel } from './models/buildGlobalModel' +import buildSchema from './models/buildSchema' +import getBuildQueryPlugin from './queries/buildQuery' - const versionSchema = buildSchema( - this.payload.config, - versionCollectionFields, - { - disableUnique: true, - draftsEnabled: true, - options: { - timestamps: false, - minimize: false, - }, - }, - ); +export const init: Init = async function init(this: MongooseAdapter) { + this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => { + const schema = buildCollectionSchema(collection, this.payload.config) - if (collection.indexes) { - collection.indexes.forEach((index) => { - // prefix 'version.' to each field in the index - const versionIndex = { - fields: {}, - options: index.options, - }; - Object.entries(index.fields) - .forEach(([key, value]) => { - versionIndex.fields[`version.${key}`] = value; - }); - versionSchema.index(versionIndex.fields, versionIndex.options); - }); - } + if (collection.versions) { + const versionModelName = getVersionsModelName(collection) - versionSchema.plugin(paginate, { useEstimatedCount: true }) - .plugin( - getBuildQueryPlugin({ - collectionSlug: collection.slug, - versionsFields: versionCollectionFields, - }), - ); + const versionCollectionFields = buildVersionCollectionFields(collection) - if (collection.versions?.drafts) { - versionSchema.plugin(mongooseAggregatePaginate); - } + const versionSchema = buildSchema(this.payload.config, versionCollectionFields, { + disableUnique: true, + draftsEnabled: true, + options: { + minimize: false, + timestamps: false, + }, + }) - const model = mongoose.model( - versionModelName, - versionSchema, - versionModelName, - ) as CollectionModel; - // this.payload.versions[collection.slug] = model; - this.versions[collection.slug] = model; + if (collection.indexes) { + collection.indexes.forEach((index) => { + // prefix 'version.' to each field in the index + const versionIndex = { + fields: {}, + options: index.options, + } + Object.entries(index.fields).forEach(([key, value]) => { + versionIndex.fields[`version.${key}`] = value + }) + versionSchema.index(versionIndex.fields, versionIndex.options) + }) + } + + versionSchema.plugin(paginate, { useEstimatedCount: true }).plugin( + getBuildQueryPlugin({ + collectionSlug: collection.slug, + versionsFields: versionCollectionFields, + }), + ) + + if (collection.versions?.drafts) { + versionSchema.plugin(mongooseAggregatePaginate) } const model = mongoose.model( - collection.slug, - schema, - this.autoPluralization === true ? undefined : collection.slug, - ) as CollectionModel; - this.collections[collection.slug] = model; + versionModelName, + versionSchema, + versionModelName, + ) as CollectionModel + // this.payload.versions[collection.slug] = model; + this.versions[collection.slug] = model + } - // TS expect error only needed until we launch 2.0.0 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - this.payload.collections[collection.slug] = { - config: collection, - }; - }, - ); + const model = mongoose.model( + collection.slug, + schema, + this.autoPluralization === true ? undefined : collection.slug, + ) as CollectionModel + this.collections[collection.slug] = model - const model = buildGlobalModel(this.payload.config); - this.globals = model; + // TS expect error only needed until we launch 2.0.0 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + this.payload.collections[collection.slug] = { + config: collection, + } + }) + + const model = buildGlobalModel(this.payload.config) + this.globals = model this.payload.config.globals.forEach((global) => { if (global.versions) { - const versionModelName = getVersionsModelName(global); + const versionModelName = getVersionsModelName(global) - const versionGlobalFields = buildVersionGlobalFields(global); + const versionGlobalFields = buildVersionGlobalFields(global) - const versionSchema = buildSchema( - this.payload.config, - versionGlobalFields, - { - indexSortableFields: this.payload.config.indexSortableFields, - disableUnique: true, - draftsEnabled: true, - options: { - timestamps: false, - minimize: false, - }, + const versionSchema = buildSchema(this.payload.config, versionGlobalFields, { + disableUnique: true, + draftsEnabled: true, + indexSortableFields: this.payload.config.indexSortableFields, + options: { + minimize: false, + timestamps: false, }, - ); + }) versionSchema .plugin(paginate, { useEstimatedCount: true }) - .plugin(getBuildQueryPlugin({ versionsFields: versionGlobalFields })); + .plugin(getBuildQueryPlugin({ versionsFields: versionGlobalFields })) const versionsModel = mongoose.model( versionModelName, versionSchema, versionModelName, - ) as CollectionModel; - this.versions[global.slug] = versionsModel; + ) as CollectionModel + this.versions[global.slug] = versionsModel } - }); -}; + }) +} diff --git a/packages/db-mongodb/src/mock.js b/packages/db-mongodb/src/mock.js index b883de34a..5deea8137 100644 --- a/packages/db-mongodb/src/mock.js +++ b/packages/db-mongodb/src/mock.js @@ -1 +1 @@ -exports.mongooseAdapter = () => ({}); +exports.mongooseAdapter = () => ({}) diff --git a/packages/db-mongodb/src/models/buildCollectionSchema.ts b/packages/db-mongodb/src/models/buildCollectionSchema.ts index ab63b4fbb..b1bd5eb00 100644 --- a/packages/db-mongodb/src/models/buildCollectionSchema.ts +++ b/packages/db-mongodb/src/models/buildCollectionSchema.ts @@ -1,38 +1,41 @@ -import paginate from 'mongoose-paginate-v2'; -import { PaginateOptions, Schema } from 'mongoose'; -import { SanitizedConfig } from 'payload/config'; -import { SanitizedCollectionConfig } from 'payload/types'; -import getBuildQueryPlugin from '../queries/buildQuery'; -import buildSchema from './buildSchema'; +import type { PaginateOptions, Schema } from 'mongoose' +import type { SanitizedConfig } from 'payload/config' +import type { SanitizedCollectionConfig } from 'payload/types' -const buildCollectionSchema = (collection: SanitizedCollectionConfig, config: SanitizedConfig, schemaOptions = {}): Schema => { - const schema = buildSchema( - config, - collection.fields, - { - draftsEnabled: Boolean(typeof collection?.versions === 'object' && collection.versions.drafts), - options: { - timestamps: collection.timestamps !== false, - minimize: false, - ...schemaOptions, - }, - indexSortableFields: config.indexSortableFields, +import paginate from 'mongoose-paginate-v2' + +import getBuildQueryPlugin from '../queries/buildQuery' +import buildSchema from './buildSchema' + +const buildCollectionSchema = ( + collection: SanitizedCollectionConfig, + config: SanitizedConfig, + schemaOptions = {}, +): Schema => { + const schema = buildSchema(config, collection.fields, { + draftsEnabled: Boolean(typeof collection?.versions === 'object' && collection.versions.drafts), + indexSortableFields: config.indexSortableFields, + options: { + minimize: false, + timestamps: collection.timestamps !== false, + ...schemaOptions, }, - ); + }) if (config.indexSortableFields && collection.timestamps !== false) { - schema.index({ updatedAt: 1 }); - schema.index({ createdAt: 1 }); + schema.index({ updatedAt: 1 }) + schema.index({ createdAt: 1 }) } if (collection.indexes) { collection.indexes.forEach((index) => { - schema.index(index.fields, index.options); - }); + schema.index(index.fields, index.options) + }) } - schema.plugin(paginate, { useEstimatedCount: true }) - .plugin(getBuildQueryPlugin({ collectionSlug: collection.slug })); + schema + .plugin(paginate, { useEstimatedCount: true }) + .plugin(getBuildQueryPlugin({ collectionSlug: collection.slug })) - return schema; -}; + return schema +} -export default buildCollectionSchema; +export default buildCollectionSchema diff --git a/packages/db-mongodb/src/models/buildGlobalModel.ts b/packages/db-mongodb/src/models/buildGlobalModel.ts index 8c240a8ec..e3c6f8e0d 100644 --- a/packages/db-mongodb/src/models/buildGlobalModel.ts +++ b/packages/db-mongodb/src/models/buildGlobalModel.ts @@ -1,32 +1,34 @@ -import mongoose from 'mongoose'; -import { SanitizedConfig } from 'payload/config'; -import buildSchema from './buildSchema'; -import getBuildQueryPlugin from '../queries/buildQuery'; -import type { GlobalModel } from '../types'; +import type { SanitizedConfig } from 'payload/config' + +import mongoose from 'mongoose' + +import type { GlobalModel } from '../types' + +import getBuildQueryPlugin from '../queries/buildQuery' +import buildSchema from './buildSchema' export const buildGlobalModel = (config: SanitizedConfig): GlobalModel | null => { if (config.globals && config.globals.length > 0) { - const globalsSchema = new mongoose.Schema({}, { discriminatorKey: 'globalType', timestamps: true, minimize: false }); + const globalsSchema = new mongoose.Schema( + {}, + { discriminatorKey: 'globalType', minimize: false, timestamps: true }, + ) - globalsSchema.plugin(getBuildQueryPlugin()); + globalsSchema.plugin(getBuildQueryPlugin()) - const Globals = mongoose.model('globals', globalsSchema, 'globals') as unknown as GlobalModel; + const Globals = mongoose.model('globals', globalsSchema, 'globals') as unknown as GlobalModel Object.values(config.globals).forEach((globalConfig) => { - const globalSchema = buildSchema( - config, - globalConfig.fields, - { - options: { - minimize: false, - }, + const globalSchema = buildSchema(config, globalConfig.fields, { + options: { + minimize: false, }, - ); - Globals.discriminator(globalConfig.slug, globalSchema); - }); + }) + Globals.discriminator(globalConfig.slug, globalSchema) + }) - return Globals; + return Globals } - return null; -}; + return null +} diff --git a/packages/db-mongodb/src/models/buildSchema.ts b/packages/db-mongodb/src/models/buildSchema.ts index 80211c8cd..619e399e0 100644 --- a/packages/db-mongodb/src/models/buildSchema.ts +++ b/packages/db-mongodb/src/models/buildSchema.ts @@ -2,9 +2,9 @@ /* eslint-disable class-methods-use-this */ /* eslint-disable @typescript-eslint/no-use-before-define */ /* eslint-disable no-use-before-define */ -import { IndexOptions, Schema, SchemaOptions, SchemaTypeOptions } from 'mongoose'; -import { SanitizedConfig, SanitizedLocalizationConfig } from 'payload/config'; -import { +import type { IndexOptions, SchemaOptions, SchemaTypeOptions } from 'mongoose' +import type { SanitizedConfig, SanitizedLocalizationConfig } from 'payload/config' +import type { ArrayField, Block, BlockField, @@ -24,408 +24,479 @@ import { RowField, SelectField, TabsField, - TextareaField, TextField, + TextareaField, UploadField, -} from 'payload/types'; +} from 'payload/types' +import type { FieldAffectingData, NonPresentationalField, Tab, UnnamedTab } from 'payload/types' +import { Schema } from 'mongoose' import { - FieldAffectingData, fieldAffectsData, fieldIsLocalized, fieldIsPresentationalOnly, - NonPresentationalField, - Tab, tabHasName, - UnnamedTab } from 'payload/types' export type BuildSchemaOptions = { - options?: SchemaOptions allowIDField?: boolean disableUnique?: boolean draftsEnabled?: boolean indexSortableFields?: boolean + options?: SchemaOptions } -type FieldSchemaGenerator = (field: Field, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions) => void; +type FieldSchemaGenerator = ( + field: Field, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, +) => void const formatBaseSchema = (field: FieldAffectingData, buildSchemaOptions: BuildSchemaOptions) => { - const { disableUnique, draftsEnabled, indexSortableFields } = buildSchemaOptions; + const { disableUnique, draftsEnabled, indexSortableFields } = buildSchemaOptions const schema: SchemaTypeOptions = { - unique: (!disableUnique && field.unique) || false, - required: false, index: field.index || (!disableUnique && field.unique) || indexSortableFields || false, - }; + required: false, + unique: (!disableUnique && field.unique) || false, + } - if ((schema.unique && (field.localized || draftsEnabled))) { - schema.sparse = true; + if (schema.unique && (field.localized || draftsEnabled)) { + schema.sparse = true } if (field.hidden) { - schema.hidden = true; + schema.hidden = true } - return schema; -}; + return schema +} -const localizeSchema = (entity: NonPresentationalField | Tab, schema, localization: false | SanitizedLocalizationConfig) => { +const localizeSchema = ( + entity: NonPresentationalField | Tab, + schema, + localization: SanitizedLocalizationConfig | false, +) => { if (fieldIsLocalized(entity) && localization && Array.isArray(localization.locales)) { return { - type: localization.localeCodes.reduce((localeSchema, locale) => ({ - ...localeSchema, - [locale]: schema, - }), { - _id: false, - }), localized: true, - }; + type: localization.localeCodes.reduce( + (localeSchema, locale) => ({ + ...localeSchema, + [locale]: schema, + }), + { + _id: false, + }, + ), + } } - return schema; -}; + return schema +} -const buildSchema = (config: SanitizedConfig, configFields: Field[], buildSchemaOptions: BuildSchemaOptions = {}): Schema => { - const { allowIDField, options } = buildSchemaOptions; - let fields = {}; +const buildSchema = ( + config: SanitizedConfig, + configFields: Field[], + buildSchemaOptions: BuildSchemaOptions = {}, +): Schema => { + const { allowIDField, options } = buildSchemaOptions + let fields = {} - let schemaFields = configFields; + let schemaFields = configFields if (!allowIDField) { - const idField = schemaFields.find((field) => fieldAffectsData(field) && field.name === 'id'); + const idField = schemaFields.find((field) => fieldAffectsData(field) && field.name === 'id') if (idField) { fields = { _id: idField.type === 'number' ? Number : String, - }; - schemaFields = schemaFields.filter((field) => !(fieldAffectsData(field) && field.name === 'id')); + } + schemaFields = schemaFields.filter( + (field) => !(fieldAffectsData(field) && field.name === 'id'), + ) } } - const schema = new Schema(fields, options); + const schema = new Schema(fields, options) schemaFields.forEach((field) => { if (!fieldIsPresentationalOnly(field)) { - const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[field.type]; + const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[field.type] if (addFieldSchema) { - addFieldSchema(field, schema, config, buildSchemaOptions); + addFieldSchema(field, schema, config, buildSchemaOptions) } } - }); + }) - return schema; -}; + return schema +} const fieldToSchemaMap: Record = { - number: (field: NumberField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { - const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: field.hasMany ? [Number] : Number }; - - schema.add({ - [field.name]: localizeSchema(field, baseSchema, config.localization), - }); - }, - text: (field: TextField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { - const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }; - - schema.add({ - [field.name]: localizeSchema(field, baseSchema, config.localization), - }); - }, - email: (field: EmailField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { - const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }; - - schema.add({ - [field.name]: localizeSchema(field, baseSchema, config.localization), - }); - }, - textarea: (field: TextareaField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { - const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }; - - schema.add({ - [field.name]: localizeSchema(field, baseSchema, config.localization), - }); - }, - richText: (field: RichTextField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { - const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Schema.Types.Mixed }; - - schema.add({ - [field.name]: localizeSchema(field, baseSchema, config.localization), - }); - }, - code: (field: CodeField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { - const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }; - - schema.add({ - [field.name]: localizeSchema(field, baseSchema, config.localization), - }); - }, - json: (field: JSONField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { - const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Schema.Types.Mixed }; - - schema.add({ - [field.name]: localizeSchema(field, baseSchema, config.localization), - }); - }, - point: (field: PointField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { - const baseSchema: SchemaTypeOptions = { - type: { - type: String, - enum: ['Point'], - }, - coordinates: { - type: [Number], - required: false, - default: field.defaultValue || undefined, - }, - }; - if (buildSchemaOptions.disableUnique && field.unique && field.localized) { - baseSchema.coordinates.sparse = true; + array: ( + field: ArrayField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ) => { + const baseSchema = { + ...formatBaseSchema(field, buildSchemaOptions), + default: undefined, + type: [ + buildSchema(config, field.fields, { + allowIDField: true, + disableUnique: buildSchemaOptions.disableUnique, + draftsEnabled: buildSchemaOptions.draftsEnabled, + options: { + _id: false, + id: false, + minimize: false, + }, + }), + ], } schema.add({ [field.name]: localizeSchema(field, baseSchema, config.localization), - }); + }) + }, + blocks: ( + field: BlockField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { + const fieldSchema = { + default: undefined, + type: [new Schema({}, { _id: false, discriminatorKey: 'blockType' })], + } + + schema.add({ + [field.name]: localizeSchema(field, fieldSchema, config.localization), + }) + + field.blocks.forEach((blockItem: Block) => { + const blockSchema = new Schema({}, { _id: false, id: false }) + + blockItem.fields.forEach((blockField) => { + const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[blockField.type] + if (addFieldSchema) { + addFieldSchema(blockField, blockSchema, config, buildSchemaOptions) + } + }) + + if (field.localized && config.localization) { + config.localization.localeCodes.forEach((localeCode) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Possible incorrect typing in mongoose types, this works + schema.path(`${field.name}.${localeCode}`).discriminator(blockItem.slug, blockSchema) + }) + } else { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Possible incorrect typing in mongoose types, this works + schema.path(field.name).discriminator(blockItem.slug, blockSchema) + } + }) + }, + checkbox: ( + field: CheckboxField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { + const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Boolean } + + schema.add({ + [field.name]: localizeSchema(field, baseSchema, config.localization), + }) + }, + code: ( + field: CodeField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { + const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String } + + schema.add({ + [field.name]: localizeSchema(field, baseSchema, config.localization), + }) + }, + collapsible: ( + field: CollapsibleField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { + field.fields.forEach((subField: Field) => { + const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type] + + if (addFieldSchema) { + addFieldSchema(subField, schema, config, buildSchemaOptions) + } + }) + }, + date: ( + field: DateField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { + const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Date } + + schema.add({ + [field.name]: localizeSchema(field, baseSchema, config.localization), + }) + }, + email: ( + field: EmailField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { + const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String } + + schema.add({ + [field.name]: localizeSchema(field, baseSchema, config.localization), + }) + }, + group: ( + field: GroupField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { + const formattedBaseSchema = formatBaseSchema(field, buildSchemaOptions) + + // carry indexSortableFields through to versions if drafts enabled + const indexSortableFields = + buildSchemaOptions.indexSortableFields && + field.name === 'version' && + buildSchemaOptions.draftsEnabled + + const baseSchema = { + ...formattedBaseSchema, + type: buildSchema(config, field.fields, { + disableUnique: buildSchemaOptions.disableUnique, + draftsEnabled: buildSchemaOptions.draftsEnabled, + indexSortableFields, + options: { + _id: false, + id: false, + minimize: false, + }, + }), + } + + schema.add({ + [field.name]: localizeSchema(field, baseSchema, config.localization), + }) + }, + json: ( + field: JSONField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { + const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Schema.Types.Mixed } + + schema.add({ + [field.name]: localizeSchema(field, baseSchema, config.localization), + }) + }, + number: ( + field: NumberField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { + const baseSchema = { + ...formatBaseSchema(field, buildSchemaOptions), + type: field.hasMany ? [Number] : Number, + } + + schema.add({ + [field.name]: localizeSchema(field, baseSchema, config.localization), + }) + }, + point: ( + field: PointField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { + const baseSchema: SchemaTypeOptions = { + coordinates: { + default: field.defaultValue || undefined, + required: false, + type: [Number], + }, + type: { + enum: ['Point'], + type: String, + }, + } + if (buildSchemaOptions.disableUnique && field.unique && field.localized) { + baseSchema.coordinates.sparse = true + } + + schema.add({ + [field.name]: localizeSchema(field, baseSchema, config.localization), + }) if (field.index === true || field.index === undefined) { - const indexOptions: IndexOptions = {}; + const indexOptions: IndexOptions = {} if (!buildSchemaOptions.disableUnique && field.unique) { - indexOptions.sparse = true; - indexOptions.unique = true; + indexOptions.sparse = true + indexOptions.unique = true } if (field.localized && config.localization) { config.localization.locales.forEach((locale) => { - schema.index({ [`${field.name}.${locale}`]: '2dsphere' }, indexOptions); - }); + schema.index({ [`${field.name}.${locale}`]: '2dsphere' }, indexOptions) + }) } else { - schema.index({ [field.name]: '2dsphere' }, indexOptions); + schema.index({ [field.name]: '2dsphere' }, indexOptions) } } }, - radio: (field: RadioField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { + radio: ( + field: RadioField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), - type: String, enum: field.options.map((option) => { - if (typeof option === 'object') return option.value; - return option; + if (typeof option === 'object') return option.value + return option }), - }; + type: String, + } schema.add({ [field.name]: localizeSchema(field, baseSchema, config.localization), - }); + }) }, - checkbox: (field: CheckboxField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { - const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Boolean }; - - schema.add({ - [field.name]: localizeSchema(field, baseSchema, config.localization), - }); - }, - date: (field: DateField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { - const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Date }; - - schema.add({ - [field.name]: localizeSchema(field, baseSchema, config.localization), - }); - }, - upload: (field: UploadField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { - const baseSchema = { - ...formatBaseSchema(field, buildSchemaOptions), - type: Schema.Types.Mixed, - ref: field.relationTo, - }; - - schema.add({ - [field.name]: localizeSchema(field, baseSchema, config.localization), - }); - }, - relationship: (field: RelationshipField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions) => { - const hasManyRelations = Array.isArray(field.relationTo); - let schemaToReturn: { [key: string]: any } = {}; + relationship: ( + field: RelationshipField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ) => { + const hasManyRelations = Array.isArray(field.relationTo) + let schemaToReturn: { [key: string]: any } = {} if (field.localized && config.localization) { schemaToReturn = { + localized: true, type: config.localization.localeCodes.reduce((locales, locale) => { - let localeSchema: { [key: string]: any } = {}; + let localeSchema: { [key: string]: any } = {} if (hasManyRelations) { localeSchema = { ...formatBaseSchema(field, buildSchemaOptions), - type: Schema.Types.Mixed, _id: false, + relationTo: { enum: field.relationTo, type: String }, + type: Schema.Types.Mixed, value: { - type: Schema.Types.Mixed, refPath: `${field.name}.${locale}.relationTo`, + type: Schema.Types.Mixed, }, - relationTo: { type: String, enum: field.relationTo }, - }; + } } else { localeSchema = { ...formatBaseSchema(field, buildSchemaOptions), - type: Schema.Types.Mixed, ref: field.relationTo, - }; + type: Schema.Types.Mixed, + } } return { ...locales, - [locale]: field.hasMany ? { type: [localeSchema], default: undefined } : localeSchema, - }; + [locale]: field.hasMany ? { default: undefined, type: [localeSchema] } : localeSchema, + } }, {}), - localized: true, - }; + } } else if (hasManyRelations) { schemaToReturn = { ...formatBaseSchema(field, buildSchemaOptions), - type: Schema.Types.Mixed, _id: false, + relationTo: { enum: field.relationTo, type: String }, + type: Schema.Types.Mixed, value: { - type: Schema.Types.Mixed, refPath: `${field.name}.relationTo`, + type: Schema.Types.Mixed, }, - relationTo: { type: String, enum: field.relationTo }, - }; + } if (field.hasMany) { schemaToReturn = { - type: [schemaToReturn], default: undefined, - }; + type: [schemaToReturn], + } } } else { schemaToReturn = { ...formatBaseSchema(field, buildSchemaOptions), - type: Schema.Types.Mixed, ref: field.relationTo, - }; + type: Schema.Types.Mixed, + } if (field.hasMany) { schemaToReturn = { - type: [schemaToReturn], default: undefined, - }; + type: [schemaToReturn], + } } } schema.add({ [field.name]: schemaToReturn, - }); + }) }, - row: (field: RowField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { - field.fields.forEach((subField: Field) => { - const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]; - - if (addFieldSchema) { - addFieldSchema(subField, schema, config, buildSchemaOptions); - } - }); - }, - collapsible: (field: CollapsibleField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { - field.fields.forEach((subField: Field) => { - const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]; - - if (addFieldSchema) { - addFieldSchema(subField, schema, config, buildSchemaOptions); - } - }); - }, - tabs: (field: TabsField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { - field.tabs.forEach((tab) => { - if (tabHasName(tab)) { - const baseSchema = { - type: buildSchema( - config, - tab.fields, - { - options: { - _id: false, - id: false, - minimize: false, - }, - disableUnique: buildSchemaOptions.disableUnique, - draftsEnabled: buildSchemaOptions.draftsEnabled, - }, - ), - }; - - schema.add({ - [tab.name]: localizeSchema(tab, baseSchema, config.localization), - }); - } else { - (tab as UnnamedTab).fields.forEach((subField: Field) => { - const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]; - - if (addFieldSchema) { - addFieldSchema(subField, schema, config, buildSchemaOptions); - } - }); - } - }); - }, - array: (field: ArrayField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions) => { - const baseSchema = { - ...formatBaseSchema(field, buildSchemaOptions), - default: undefined, - type: [buildSchema( - config, - field.fields, - { - options: { - _id: false, - id: false, - minimize: false, - }, - allowIDField: true, - disableUnique: buildSchemaOptions.disableUnique, - draftsEnabled: buildSchemaOptions.draftsEnabled, - }, - )], - }; + richText: ( + field: RichTextField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { + const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Schema.Types.Mixed } schema.add({ [field.name]: localizeSchema(field, baseSchema, config.localization), - }); + }) }, - group: (field: GroupField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { - const formattedBaseSchema = formatBaseSchema(field, buildSchemaOptions); + row: ( + field: RowField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { + field.fields.forEach((subField: Field) => { + const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type] - // carry indexSortableFields through to versions if drafts enabled - const indexSortableFields = (buildSchemaOptions.indexSortableFields && field.name === 'version' && buildSchemaOptions.draftsEnabled); - - const baseSchema = { - ...formattedBaseSchema, - type: buildSchema( - config, - field.fields, - { - options: { - _id: false, - id: false, - minimize: false, - }, - indexSortableFields, - disableUnique: buildSchemaOptions.disableUnique, - draftsEnabled: buildSchemaOptions.draftsEnabled, - }, - ), - }; - - schema.add({ - [field.name]: localizeSchema(field, baseSchema, config.localization), - }); + if (addFieldSchema) { + addFieldSchema(subField, schema, config, buildSchemaOptions) + } + }) }, - select: (field: SelectField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { + select: ( + field: SelectField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), - type: String, enum: field.options.map((option) => { - if (typeof option === 'object') return option.value; - return option; + if (typeof option === 'object') return option.value + return option }), - }; + type: String, + } if (buildSchemaOptions.draftsEnabled || !field.required) { - baseSchema.enum.push(null); + baseSchema.enum.push(null) } schema.add({ @@ -434,41 +505,82 @@ const fieldToSchemaMap: Record = { field.hasMany ? [baseSchema] : baseSchema, config.localization, ), - }); + }) }, - blocks: (field: BlockField, schema: Schema, config: SanitizedConfig, buildSchemaOptions: BuildSchemaOptions): void => { - const fieldSchema = { - default: undefined, - type: [new Schema({}, { _id: false, discriminatorKey: 'blockType' })], - }; + tabs: ( + field: TabsField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { + field.tabs.forEach((tab) => { + if (tabHasName(tab)) { + const baseSchema = { + type: buildSchema(config, tab.fields, { + disableUnique: buildSchemaOptions.disableUnique, + draftsEnabled: buildSchemaOptions.draftsEnabled, + options: { + _id: false, + id: false, + minimize: false, + }, + }), + } + + schema.add({ + [tab.name]: localizeSchema(tab, baseSchema, config.localization), + }) + } else { + tab.fields.forEach((subField: Field) => { + const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type] + + if (addFieldSchema) { + addFieldSchema(subField, schema, config, buildSchemaOptions) + } + }) + } + }) + }, + text: ( + field: TextField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { + const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String } schema.add({ - [field.name]: localizeSchema(field, fieldSchema, config.localization), - }); - - field.blocks.forEach((blockItem: Block) => { - const blockSchema = new Schema({}, { _id: false, id: false }); - - blockItem.fields.forEach((blockField) => { - const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[blockField.type]; - if (addFieldSchema) { - addFieldSchema(blockField, blockSchema, config, buildSchemaOptions); - } - }); - - if (field.localized && config.localization) { - config.localization.localeCodes.forEach((localeCode) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore Possible incorrect typing in mongoose types, this works - schema.path(`${field.name}.${localeCode}`).discriminator(blockItem.slug, blockSchema); - }); - } else { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore Possible incorrect typing in mongoose types, this works - schema.path(field.name).discriminator(blockItem.slug, blockSchema); - } - }); + [field.name]: localizeSchema(field, baseSchema, config.localization), + }) }, -}; + textarea: ( + field: TextareaField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { + const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String } -export default buildSchema; + schema.add({ + [field.name]: localizeSchema(field, baseSchema, config.localization), + }) + }, + upload: ( + field: UploadField, + schema: Schema, + config: SanitizedConfig, + buildSchemaOptions: BuildSchemaOptions, + ): void => { + const baseSchema = { + ...formatBaseSchema(field, buildSchemaOptions), + ref: field.relationTo, + type: Schema.Types.Mixed, + } + + schema.add({ + [field.name]: localizeSchema(field, baseSchema, config.localization), + }) + }, +} + +export default buildSchema diff --git a/packages/db-mongodb/src/queries/buildAndOrConditions.ts b/packages/db-mongodb/src/queries/buildAndOrConditions.ts index 15d852a52..340f6ad3c 100644 --- a/packages/db-mongodb/src/queries/buildAndOrConditions.ts +++ b/packages/db-mongodb/src/queries/buildAndOrConditions.ts @@ -1,23 +1,24 @@ -import { Where, Field } from 'payload/types'; -import { Payload } from 'payload'; -import { parseParams } from './parseParams'; +import type { Payload } from 'payload' +import type { Field, Where } from 'payload/types' + +import { parseParams } from './parseParams' export async function buildAndOrConditions({ - where, collectionSlug, - globalSlug, - payload, - locale, fields, + globalSlug, + locale, + payload, + where, }: { - where: Where[], - collectionSlug?: string, - globalSlug?: string, - payload: Payload, - locale?: string, - fields: Field[], + collectionSlug?: string + fields: Field[] + globalSlug?: string + locale?: string + payload: Payload + where: Where[] }): Promise[]> { - const completedConditions = []; + const completedConditions = [] // Loop over all AND / OR operations and add them to the AND / OR query param // Operations should come through as an array // eslint-disable-next-line no-restricted-syntax @@ -26,17 +27,17 @@ export async function buildAndOrConditions({ if (typeof condition === 'object') { // eslint-disable-next-line no-await-in-loop const result = await parseParams({ - where: condition, collectionSlug, - globalSlug, - payload, - locale, fields, - }); + globalSlug, + locale, + payload, + where: condition, + }) if (Object.keys(result).length > 0) { - completedConditions.push(result); + completedConditions.push(result) } } } - return completedConditions; + return completedConditions } diff --git a/packages/db-mongodb/src/queries/buildQuery.ts b/packages/db-mongodb/src/queries/buildQuery.ts index bda485e9b..f250d5f88 100644 --- a/packages/db-mongodb/src/queries/buildQuery.ts +++ b/packages/db-mongodb/src/queries/buildQuery.ts @@ -1,7 +1,9 @@ -import { Where, Field } from 'payload/types'; -import { QueryError } from 'payload/errors'; -import { Payload } from 'payload'; -import { parseParams } from './parseParams'; +import type { Payload } from 'payload' +import type { Field, Where } from 'payload/types' + +import { QueryError } from 'payload/errors' + +import { parseParams } from './parseParams' type GetBuildQueryPluginArgs = { collectionSlug?: string @@ -9,50 +11,52 @@ type GetBuildQueryPluginArgs = { } export type BuildQueryArgs = { - payload: Payload - locale?: string - where: Where globalSlug?: string + locale?: string + payload: Payload + where: Where } // This plugin asynchronously builds a list of Mongoose query constraints // which can then be used in subsequent Mongoose queries. -const getBuildQueryPlugin = ({ - collectionSlug, - versionsFields, -}: GetBuildQueryPluginArgs = {}) => { +const getBuildQueryPlugin = ({ collectionSlug, versionsFields }: GetBuildQueryPluginArgs = {}) => { return function buildQueryPlugin(schema) { - const modifiedSchema = schema; - async function buildQuery({ payload, locale, where, globalSlug }: BuildQueryArgs): Promise> { - let fields = versionsFields; + const modifiedSchema = schema + async function buildQuery({ + globalSlug, + locale, + payload, + where, + }: BuildQueryArgs): Promise> { + let fields = versionsFields if (!fields) { if (globalSlug) { - const globalConfig = payload.globals.config.find(({ slug }) => slug === globalSlug); - fields = globalConfig.fields; + const globalConfig = payload.globals.config.find(({ slug }) => slug === globalSlug) + fields = globalConfig.fields } if (collectionSlug) { - const collectionConfig = payload.collections[collectionSlug].config; - fields = collectionConfig.fields; + const collectionConfig = payload.collections[collectionSlug].config + fields = collectionConfig.fields } } - const errors = []; + const errors = [] const result = await parseParams({ collectionSlug, fields, globalSlug, - payload, locale, + payload, where, - }); + }) if (errors.length > 0) { - throw new QueryError(errors); + throw new QueryError(errors) } - return result; + return result } - modifiedSchema.statics.buildQuery = buildQuery; - }; -}; + modifiedSchema.statics.buildQuery = buildQuery + } +} -export default getBuildQueryPlugin; +export default getBuildQueryPlugin diff --git a/packages/db-mongodb/src/queries/buildSearchParams.ts b/packages/db-mongodb/src/queries/buildSearchParams.ts index a6ee12b1a..59df2b770 100644 --- a/packages/db-mongodb/src/queries/buildSearchParams.ts +++ b/packages/db-mongodb/src/queries/buildSearchParams.ts @@ -1,238 +1,247 @@ -import mongoose from 'mongoose'; -import objectID from 'bson-objectid'; -import { getLocalizedPaths } from 'payload/database'; -import { Field, fieldAffectsData } from 'payload/types'; -import { PathToQuery } from 'payload/database'; -import { validOperators } from 'payload/types'; -import { Payload } from 'payload'; -import { Operator } from 'payload/types'; -import { operatorMap } from './operatorMap'; -import { sanitizeQueryValue } from './sanitizeQueryValue'; -import { MongooseAdapter } from '..'; +import type { Payload } from 'payload' +import type { PathToQuery } from 'payload/database' +import type { Field } from 'payload/types' +import type { Operator } from 'payload/types' + +import objectID from 'bson-objectid' +import mongoose from 'mongoose' +import { getLocalizedPaths } from 'payload/database' +import { fieldAffectsData } from 'payload/types' +import { validOperators } from 'payload/types' + +import type { MongooseAdapter } from '..' + +import { operatorMap } from './operatorMap' +import { sanitizeQueryValue } from './sanitizeQueryValue' type SearchParam = { - path?: string, - value: unknown, + path?: string + value: unknown } const subQueryOptions = { - limit: 50, lean: true, -}; + limit: 50, +} /** * Convert the Payload key / value / operator into a MongoDB query */ export async function buildSearchParam({ - fields, - incomingPath, - val, - operator, collectionSlug, + fields, globalSlug, - payload, + incomingPath, locale, + operator, + payload, + val, }: { - fields: Field[], - incomingPath: string, - val: unknown, - operator: string - collectionSlug?: string, - globalSlug?: string, - payload: Payload, + collectionSlug?: string + fields: Field[] + globalSlug?: string + incomingPath: string locale?: string + operator: string + payload: Payload + val: unknown }): Promise { // Replace GraphQL nested field double underscore formatting - let sanitizedPath = incomingPath.replace(/__/gi, '.'); - if (sanitizedPath === 'id') sanitizedPath = '_id'; + let sanitizedPath = incomingPath.replace(/__/g, '.') + if (sanitizedPath === 'id') sanitizedPath = '_id' - let paths: PathToQuery[] = []; + let paths: PathToQuery[] = [] - let hasCustomID = false; + let hasCustomID = false if (sanitizedPath === '_id') { - const customIDfield = payload.collections[collectionSlug]?.config.fields.find((field) => fieldAffectsData(field) && field.name === 'id'); + const customIDfield = payload.collections[collectionSlug]?.config.fields.find( + (field) => fieldAffectsData(field) && field.name === 'id', + ) - let idFieldType: 'text' | 'number' = 'text'; + let idFieldType: 'number' | 'text' = 'text' if (customIDfield) { if (customIDfield?.type === 'text' || customIDfield?.type === 'number') { - idFieldType = customIDfield.type; + idFieldType = customIDfield.type } - hasCustomID = true; + hasCustomID = true } paths.push({ - path: '_id', + collectionSlug, + complete: true, field: { name: 'id', type: idFieldType, } as Field, - complete: true, - collectionSlug, - }); + path: '_id', + }) } else { paths = await getLocalizedPaths({ - payload, - locale, collectionSlug, - globalSlug, fields, + globalSlug, incomingPath: sanitizedPath, - }); + locale, + payload, + }) } - const [{ - path, - field, - }] = paths; + const [{ field, path }] = paths if (path) { const formattedValue = sanitizeQueryValue({ field, - path, - operator, - val, hasCustomID, - }); + operator, + path, + val, + }) // If there are multiple collections to search through, // Recursively build up a list of query constraints if (paths.length > 1) { // Remove top collection and reverse array // to work backwards from top - const pathsToQuery = paths.slice(1) - .reverse(); + const pathsToQuery = paths.slice(1).reverse() const initialRelationshipQuery = { value: {}, - } as SearchParam; + } as SearchParam - const relationshipQuery = await pathsToQuery.reduce(async (priorQuery, { - path: subPath, - collectionSlug: slug, - }, i) => { - const priorQueryResult = await priorQuery; + const relationshipQuery = await pathsToQuery.reduce( + async (priorQuery, { collectionSlug: slug, path: subPath }, i) => { + const priorQueryResult = await priorQuery - const SubModel = (payload.db as MongooseAdapter).collections[slug]; + const SubModel = (payload.db as MongooseAdapter).collections[slug] - // On the "deepest" collection, - // Search on the value passed through the query - if (i === 0) { - const subQuery = await SubModel.buildQuery({ - where: { - [subPath]: { - [operator]: val, + // On the "deepest" collection, + // Search on the value passed through the query + if (i === 0) { + const subQuery = await SubModel.buildQuery({ + locale, + payload, + where: { + [subPath]: { + [operator]: val, + }, }, - }, - payload, - locale, - }); + }) - const result = await SubModel.find(subQuery, subQueryOptions); + const result = await SubModel.find(subQuery, subQueryOptions) - const $in: unknown[] = []; + const $in: unknown[] = [] - result.forEach((doc) => { - const stringID = doc._id.toString(); - $in.push(stringID); + result.forEach((doc) => { + const stringID = doc._id.toString() + $in.push(stringID) - if (mongoose.Types.ObjectId.isValid(stringID)) { - $in.push(doc._id); + if (mongoose.Types.ObjectId.isValid(stringID)) { + $in.push(doc._id) + } + }) + + if (pathsToQuery.length === 1) { + return { + path, + value: { $in }, + } } - }); - if (pathsToQuery.length === 1) { + const nextSubPath = pathsToQuery[i + 1].path + + return { + value: { [nextSubPath]: { $in } }, + } + } + + const subQuery = priorQueryResult.value + const result = await SubModel.find(subQuery, subQueryOptions) + + const $in = result.map((doc) => doc._id.toString()) + + // If it is the last recursion + // then pass through the search param + if (i + 1 === pathsToQuery.length) { return { path, value: { $in }, - }; + } } - const nextSubPath = pathsToQuery[i + 1].path; - return { - value: { [nextSubPath]: { $in } }, - }; - } + value: { + _id: { $in }, + }, + } + }, + Promise.resolve(initialRelationshipQuery), + ) - const subQuery = priorQueryResult.value; - const result = await SubModel.find(subQuery, subQueryOptions); - - const $in = result.map((doc) => doc._id.toString()); - - // If it is the last recursion - // then pass through the search param - if (i + 1 === pathsToQuery.length) { - return { - path, - value: { $in }, - }; - } - - return { - value: { - _id: { $in }, - }, - }; - }, Promise.resolve(initialRelationshipQuery)); - - return relationshipQuery; + return relationshipQuery } if (operator && validOperators.includes(operator as Operator)) { - const operatorKey = operatorMap[operator]; + const operatorKey = operatorMap[operator] if (field.type === 'relationship' || field.type === 'upload') { - let hasNumberIDRelation; + let hasNumberIDRelation const result = { value: { - $or: [ - { [path]: { [operatorKey]: formattedValue } }, - ], + $or: [{ [path]: { [operatorKey]: formattedValue } }], }, - }; + } if (typeof formattedValue === 'string') { if (mongoose.Types.ObjectId.isValid(formattedValue)) { - result.value.$or.push({ [path]: { [operatorKey]: objectID(formattedValue) } }); + result.value.$or.push({ [path]: { [operatorKey]: objectID(formattedValue) } }) } else { - (Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo]).forEach((relationTo) => { - const isRelatedToCustomNumberID = payload.collections[relationTo]?.config?.fields.find((relatedField) => { - return fieldAffectsData(relatedField) && relatedField.name === 'id' && relatedField.type === 'number'; - }); + ;(Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo]).forEach( + (relationTo) => { + const isRelatedToCustomNumberID = payload.collections[ + relationTo + ]?.config?.fields.find((relatedField) => { + return ( + fieldAffectsData(relatedField) && + relatedField.name === 'id' && + relatedField.type === 'number' + ) + }) - if (isRelatedToCustomNumberID) { - if (isRelatedToCustomNumberID.type === 'number') hasNumberIDRelation = true; - } - }); + if (isRelatedToCustomNumberID) { + if (isRelatedToCustomNumberID.type === 'number') hasNumberIDRelation = true + } + }, + ) - if (hasNumberIDRelation) result.value.$or.push({ [path]: { [operatorKey]: parseFloat(formattedValue) } }); + if (hasNumberIDRelation) + result.value.$or.push({ [path]: { [operatorKey]: parseFloat(formattedValue) } }) } } if (result.value.$or.length > 1) { - return result; + return result } } if (operator === 'like' && typeof formattedValue === 'string') { - const words = formattedValue.split(' '); + const words = formattedValue.split(' ') const result = { value: { $and: words.map((word) => ({ [path]: { - $regex: word.replace(/[\\^$*+?\\.()|[\]{}]/g, '\\$&'), $options: 'i', + $regex: word.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'), }, })), }, - }; + } - return result; + return result } // Some operators like 'near' need to define a full query @@ -241,14 +250,14 @@ export async function buildSearchParam({ return { path, value: formattedValue, - }; + } } return { path, value: { [operatorKey]: formattedValue }, - }; + } } } - return undefined; + return undefined } diff --git a/packages/db-mongodb/src/queries/buildSortParam.ts b/packages/db-mongodb/src/queries/buildSortParam.ts index 847ec93fe..eeed37905 100644 --- a/packages/db-mongodb/src/queries/buildSortParam.ts +++ b/packages/db-mongodb/src/queries/buildSortParam.ts @@ -1,50 +1,57 @@ -import { PaginateOptions } from 'mongoose'; -import { SanitizedConfig } from 'payload/config'; -import { Field } from 'payload/types'; -import { getLocalizedSortProperty } from './getLocalizedSortProperty'; +import type { PaginateOptions } from 'mongoose' +import type { SanitizedConfig } from 'payload/config' +import type { Field } from 'payload/types' + +import { getLocalizedSortProperty } from './getLocalizedSortProperty' type Args = { - sort: string config: SanitizedConfig fields: Field[] - timestamps: boolean locale: string + sort: string + timestamps: boolean } export type SortArgs = { - property: string direction: SortDirection + property: string }[] -export type SortDirection = 'asc' | 'desc'; +export type SortDirection = 'asc' | 'desc' -export const buildSortParam = ({ sort, config, fields, timestamps, locale }: Args): PaginateOptions['sort'] => { - let sortProperty: string; - let sortDirection: SortDirection = 'desc'; +export const buildSortParam = ({ + config, + fields, + locale, + sort, + timestamps, +}: Args): PaginateOptions['sort'] => { + let sortProperty: string + let sortDirection: SortDirection = 'desc' if (!sort) { if (timestamps) { - sortProperty = 'createdAt'; + sortProperty = 'createdAt' } else { - sortProperty = '_id'; + sortProperty = '_id' } } else if (sort.indexOf('-') === 0) { - sortProperty = sort.substring(1); + sortProperty = sort.substring(1) } else { - sortProperty = sort; - sortDirection = 'asc'; + sortProperty = sort + sortDirection = 'asc' } if (sortProperty === 'id') { - sortProperty = '_id'; + sortProperty = '_id' } else { sortProperty = getLocalizedSortProperty({ - segments: sortProperty.split('.'), config, fields, locale, - }); + segments: sortProperty.split('.'), + }) } - return { [sortProperty]: sortDirection }; -}; + return { [sortProperty]: sortDirection } +} diff --git a/packages/db-mongodb/src/queries/getLocalizedSortProperty.spec.ts b/packages/db-mongodb/src/queries/getLocalizedSortProperty.spec.ts index 40a3b0edc..92c311b5d 100644 --- a/packages/db-mongodb/src/queries/getLocalizedSortProperty.spec.ts +++ b/packages/db-mongodb/src/queries/getLocalizedSortProperty.spec.ts @@ -1,12 +1,12 @@ -import { sanitizeConfig } from 'payload/config'; -import { Config } from 'payload/config'; -import { getLocalizedSortProperty } from './getLocalizedSortProperty'; +import { sanitizeConfig } from 'payload/config' +import { Config } from 'payload/config' +import { getLocalizedSortProperty } from './getLocalizedSortProperty' const config = { localization: { locales: ['en', 'es'], }, -} as Config; +} as Config describe('get localized sort property', () => { it('passes through a non-localized sort property', () => { @@ -20,10 +20,10 @@ describe('get localized sort property', () => { }, ], locale: 'en', - }); + }) - expect(result).toStrictEqual('title'); - }); + expect(result).toStrictEqual('title') + }) it('properly localizes an un-localized sort property', () => { const result = getLocalizedSortProperty({ @@ -37,10 +37,10 @@ describe('get localized sort property', () => { }, ], locale: 'en', - }); + }) - expect(result).toStrictEqual('title.en'); - }); + expect(result).toStrictEqual('title.en') + }) it('keeps specifically asked-for localized sort properties', () => { const result = getLocalizedSortProperty({ @@ -54,10 +54,10 @@ describe('get localized sort property', () => { }, ], locale: 'en', - }); + }) - expect(result).toStrictEqual('title.es'); - }); + expect(result).toStrictEqual('title.es') + }) it('properly localizes nested sort properties', () => { const result = getLocalizedSortProperty({ @@ -77,10 +77,10 @@ describe('get localized sort property', () => { }, ], locale: 'en', - }); + }) - expect(result).toStrictEqual('group.title.en'); - }); + expect(result).toStrictEqual('group.title.en') + }) it('keeps requested locale with nested sort properties', () => { const result = getLocalizedSortProperty({ @@ -100,10 +100,10 @@ describe('get localized sort property', () => { }, ], locale: 'en', - }); + }) - expect(result).toStrictEqual('group.title.es'); - }); + expect(result).toStrictEqual('group.title.es') + }) it('properly localizes field within row', () => { const result = getLocalizedSortProperty({ @@ -122,10 +122,10 @@ describe('get localized sort property', () => { }, ], locale: 'en', - }); + }) - expect(result).toStrictEqual('title.en'); - }); + expect(result).toStrictEqual('title.en') + }) it('properly localizes field within named tab', () => { const result = getLocalizedSortProperty({ @@ -149,10 +149,10 @@ describe('get localized sort property', () => { }, ], locale: 'en', - }); + }) - expect(result).toStrictEqual('tab.title.en'); - }); + expect(result).toStrictEqual('tab.title.en') + }) it('properly localizes field within unnamed tab', () => { const result = getLocalizedSortProperty({ @@ -176,8 +176,8 @@ describe('get localized sort property', () => { }, ], locale: 'en', - }); + }) - expect(result).toStrictEqual('title.en'); - }); -}); + expect(result).toStrictEqual('title.en') + }) +}) diff --git a/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts b/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts index 4a70119b3..915046fed 100644 --- a/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts +++ b/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts @@ -1,89 +1,103 @@ -import { SanitizedConfig } from 'payload/config'; -import { Field, fieldAffectsData, fieldIsPresentationalOnly } from 'payload/types'; -import { flattenTopLevelFields } from 'payload/utilities'; +import type { SanitizedConfig } from 'payload/config' +import type { Field } from 'payload/types' + +import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/types' +import { flattenTopLevelFields } from 'payload/utilities' type Args = { - segments: string[] config: SanitizedConfig fields: Field[] locale: string result?: string + segments: string[] } export const getLocalizedSortProperty = ({ - segments: incomingSegments, config, fields: incomingFields, locale, result: incomingResult, + segments: incomingSegments, }: Args): string => { // If localization is not enabled, accept exactly // what is sent in if (!config.localization) { - return incomingSegments.join('.'); + return incomingSegments.join('.') } // Flatten incoming fields (row, etc) - const fields = flattenTopLevelFields(incomingFields); + const fields = flattenTopLevelFields(incomingFields) - const segments = [...incomingSegments]; + const segments = [...incomingSegments] // Retrieve first segment, and remove from segments - const firstSegment = segments.shift(); + const firstSegment = segments.shift() // Attempt to find a matched field - const matchedField = fields.find((field) => fieldAffectsData(field) && field.name === firstSegment); + const matchedField = fields.find( + (field) => fieldAffectsData(field) && field.name === firstSegment, + ) if (matchedField && !fieldIsPresentationalOnly(matchedField)) { - let nextFields: Field[]; - const remainingSegments = [...segments]; - let localizedSegment = matchedField.name; + let nextFields: Field[] + const remainingSegments = [...segments] + let localizedSegment = matchedField.name if (matchedField.localized) { // Check to see if next segment is a locale if (segments.length > 0) { - const nextSegmentIsLocale = config.localization.localeCodes.includes(remainingSegments[0]); + const nextSegmentIsLocale = config.localization.localeCodes.includes(remainingSegments[0]) // If next segment is locale, remove it from remaining segments // and use it to localize the current segment if (nextSegmentIsLocale) { - const nextSegment = remainingSegments.shift(); - localizedSegment = `${matchedField.name}.${nextSegment}`; + const nextSegment = remainingSegments.shift() + localizedSegment = `${matchedField.name}.${nextSegment}` } } else { // If no more segments, but field is localized, use default locale - localizedSegment = `${matchedField.name}.${locale}`; + localizedSegment = `${matchedField.name}.${locale}` } } // If there are subfields, pass them through - if (matchedField.type === 'tab' || matchedField.type === 'group' || matchedField.type === 'array') { - nextFields = matchedField.fields; + if ( + matchedField.type === 'tab' || + matchedField.type === 'group' || + matchedField.type === 'array' + ) { + nextFields = matchedField.fields } if (matchedField.type === 'blocks') { nextFields = matchedField.blocks.reduce((flattenedBlockFields, block) => { return [ ...flattenedBlockFields, - ...block.fields.filter((blockField) => (fieldAffectsData(blockField) && (blockField.name !== 'blockType' && blockField.name !== 'blockName')) || !fieldAffectsData(blockField)), - ]; - }, []); + ...block.fields.filter( + (blockField) => + (fieldAffectsData(blockField) && + blockField.name !== 'blockType' && + blockField.name !== 'blockName') || + !fieldAffectsData(blockField), + ), + ] + }, []) } - const result = incomingResult ? `${incomingResult}.${localizedSegment}` : localizedSegment; + const result = incomingResult ? `${incomingResult}.${localizedSegment}` : localizedSegment if (nextFields) { return getLocalizedSortProperty({ - segments: remainingSegments, config, fields: nextFields, locale, result, - }); + segments: remainingSegments, + }) } - return result; + return result } - return incomingSegments.join('.'); -}; + return incomingSegments.join('.') +} diff --git a/packages/db-mongodb/src/queries/operatorMap.ts b/packages/db-mongodb/src/queries/operatorMap.ts index 280defb35..e4352149d 100644 --- a/packages/db-mongodb/src/queries/operatorMap.ts +++ b/packages/db-mongodb/src/queries/operatorMap.ts @@ -1,15 +1,15 @@ export const operatorMap = { - greater_than_equal: '$gte', - less_than_equal: '$lte', - less_than: '$lt', - greater_than: '$gt', - in: '$in', all: '$all', - not_in: '$nin', - not_equals: '$ne', - exists: '$exists', equals: '$eq', - near: '$near', - within: '$geoWithin', + exists: '$exists', + greater_than: '$gt', + greater_than_equal: '$gte', + in: '$in', intersects: '$geoIntersects', -}; + less_than: '$lt', + less_than_equal: '$lte', + near: '$near', + not_equals: '$ne', + not_in: '$nin', + within: '$geoWithin', +} diff --git a/packages/db-mongodb/src/queries/parseParams.ts b/packages/db-mongodb/src/queries/parseParams.ts index c5f572a03..d65edb568 100644 --- a/packages/db-mongodb/src/queries/parseParams.ts +++ b/packages/db-mongodb/src/queries/parseParams.ts @@ -1,78 +1,80 @@ /* eslint-disable no-restricted-syntax */ /* eslint-disable no-await-in-loop */ -import { FilterQuery } from 'mongoose'; -import deepmerge from 'deepmerge'; -import { Operator, Where } from 'payload/types'; -import { combineMerge } from 'payload/utilities'; -import { Field } from 'payload/types'; -import { validOperators } from 'payload/types'; -import { Payload } from 'payload'; -import { buildSearchParam } from './buildSearchParams'; -import { buildAndOrConditions } from './buildAndOrConditions'; +import type { FilterQuery } from 'mongoose' +import type { Payload } from 'payload' +import type { Operator, Where } from 'payload/types' +import type { Field } from 'payload/types' + +import deepmerge from 'deepmerge' +import { validOperators } from 'payload/types' +import { combineMerge } from 'payload/utilities' + +import { buildAndOrConditions } from './buildAndOrConditions' +import { buildSearchParam } from './buildSearchParams' export async function parseParams({ - where, collectionSlug, - globalSlug, - payload, - locale, fields, + globalSlug, + locale, + payload, + where, }: { - where: Where, - collectionSlug?: string, - globalSlug?: string, - payload: Payload, - locale: string, - fields: Field[], + collectionSlug?: string + fields: Field[] + globalSlug?: string + locale: string + payload: Payload + where: Where }): Promise> { - let result = {} as FilterQuery; + let result = {} as FilterQuery if (typeof where === 'object') { // We need to determine if the whereKey is an AND, OR, or a schema path for (const relationOrPath of Object.keys(where)) { - const condition = where[relationOrPath]; - let conditionOperator: '$and' | '$or'; + const condition = where[relationOrPath] + let conditionOperator: '$and' | '$or' if (relationOrPath.toLowerCase() === 'and') { - conditionOperator = '$and'; + conditionOperator = '$and' } else if (relationOrPath.toLowerCase() === 'or') { - conditionOperator = '$or'; + conditionOperator = '$or' } if (Array.isArray(condition)) { const builtConditions = await buildAndOrConditions({ collectionSlug, fields, globalSlug, - payload, locale, + payload, where: condition, - }); - if (builtConditions.length > 0) result[conditionOperator] = builtConditions; + }) + if (builtConditions.length > 0) result[conditionOperator] = builtConditions } else { // It's a path - and there can be multiple comparisons on a single path. // For example - title like 'test' and title not equal to 'tester' // So we need to loop on keys again here to handle each operator independently - const pathOperators = where[relationOrPath]; + const pathOperators = where[relationOrPath] if (typeof pathOperators === 'object') { for (const operator of Object.keys(pathOperators)) { if (validOperators.includes(operator as Operator)) { const searchParam = await buildSearchParam({ collectionSlug, - globalSlug, - payload, - locale, fields, + globalSlug, incomingPath: relationOrPath, - val: pathOperators[operator], + locale, operator, - }); + payload, + val: pathOperators[operator], + }) if (searchParam?.value && searchParam?.path) { result = { ...result, [searchParam.path]: searchParam.value, - }; + } } else if (typeof searchParam?.value === 'object') { - result = deepmerge(result, searchParam.value, { arrayMerge: combineMerge }); + result = deepmerge(result, searchParam.value, { arrayMerge: combineMerge }) } } } @@ -81,5 +83,5 @@ export async function parseParams({ } } - return result; + return result } diff --git a/packages/db-mongodb/src/queries/sanitizeQueryValue.ts b/packages/db-mongodb/src/queries/sanitizeQueryValue.ts index 9ae737c6d..243e19bc6 100644 --- a/packages/db-mongodb/src/queries/sanitizeQueryValue.ts +++ b/packages/db-mongodb/src/queries/sanitizeQueryValue.ts @@ -1,127 +1,132 @@ -import mongoose from 'mongoose'; -import { Field, TabAsField } from 'payload/dist/fields/config/types'; -import { createArrayFromCommaDelineated } from '../utilities/createArrayFromCommaDelineated'; +import type { Field, TabAsField } from 'payload/dist/fields/config/types' + +import mongoose from 'mongoose' + +import { createArrayFromCommaDelineated } from '../utilities/createArrayFromCommaDelineated' type SanitizeQueryValueArgs = { field: Field | TabAsField - path: string - operator: string, - val: any hasCustomID: boolean + operator: string + path: string + val: any } -export const sanitizeQueryValue = ({ field, path, operator, val, hasCustomID }: SanitizeQueryValueArgs): unknown => { - let formattedValue = val; +export const sanitizeQueryValue = ({ + field, + hasCustomID, + operator, + path, + val, +}: SanitizeQueryValueArgs): unknown => { + let formattedValue = val // Disregard invalid _ids if (path === '_id' && typeof val === 'string' && val.split(',').length === 1) { if (!hasCustomID) { - const isValid = mongoose.Types.ObjectId.isValid(val); + const isValid = mongoose.Types.ObjectId.isValid(val) if (!isValid) { - return undefined; + return undefined } } if (field.type === 'number') { - const parsedNumber = parseFloat(val); + const parsedNumber = parseFloat(val) if (Number.isNaN(parsedNumber)) { - return undefined; + return undefined } } } // Cast incoming values as proper searchable types if (field.type === 'checkbox' && typeof val === 'string') { - if (val.toLowerCase() === 'true') formattedValue = true; - if (val.toLowerCase() === 'false') formattedValue = false; + if (val.toLowerCase() === 'true') formattedValue = true + if (val.toLowerCase() === 'false') formattedValue = false } - if (['all', 'not_in', 'in'].includes(operator) && typeof formattedValue === 'string') { - formattedValue = createArrayFromCommaDelineated(formattedValue); + if (['all', 'in', 'not_in'].includes(operator) && typeof formattedValue === 'string') { + formattedValue = createArrayFromCommaDelineated(formattedValue) if (field.type === 'number') { - formattedValue = formattedValue.map((arrayVal) => parseFloat(arrayVal)); + formattedValue = formattedValue.map((arrayVal) => parseFloat(arrayVal)) } } if (field.type === 'number' && typeof formattedValue === 'string') { - formattedValue = Number(val); + formattedValue = Number(val) } if (field.type === 'date' && typeof val === 'string') { - formattedValue = new Date(val); + formattedValue = new Date(val) if (Number.isNaN(Date.parse(formattedValue))) { - return undefined; + return undefined } } - if (['relationship', 'upload'].includes(field.type)) { if (val === 'null') { - formattedValue = null; + formattedValue = null } if (operator === 'in' && Array.isArray(formattedValue)) { formattedValue = formattedValue.reduce((formattedValues, inVal) => { - const newValues = [inVal]; - if (mongoose.Types.ObjectId.isValid(inVal)) newValues.push(new mongoose.Types.ObjectId(inVal)); + const newValues = [inVal] + if (mongoose.Types.ObjectId.isValid(inVal)) + newValues.push(new mongoose.Types.ObjectId(inVal)) - const parsedNumber = parseFloat(inVal); - if (!Number.isNaN(parsedNumber)) newValues.push(parsedNumber); + const parsedNumber = parseFloat(inVal) + if (!Number.isNaN(parsedNumber)) newValues.push(parsedNumber) - return [ - ...formattedValues, - ...newValues, - ]; - }, []); + return [...formattedValues, ...newValues] + }, []) } } // Set up specific formatting necessary by operators if (operator === 'near') { - let lng; - let lat; - let maxDistance; - let minDistance; + let lng + let lat + let maxDistance + let minDistance if (Array.isArray(formattedValue)) { - [lng, lat, maxDistance, minDistance] = formattedValue; + ;[lng, lat, maxDistance, minDistance] = formattedValue } if (typeof formattedValue === 'string') { - [lng, lat, maxDistance, minDistance] = createArrayFromCommaDelineated(formattedValue); + ;[lng, lat, maxDistance, minDistance] = createArrayFromCommaDelineated(formattedValue) } if (lng == null || lat == null || (maxDistance == null && minDistance == null)) { - formattedValue = undefined; + formattedValue = undefined } else { formattedValue = { - $geometry: { type: 'Point', coordinates: [parseFloat(lng), parseFloat(lat)] }, - }; + $geometry: { coordinates: [parseFloat(lng), parseFloat(lat)], type: 'Point' }, + } - if (maxDistance) formattedValue.$maxDistance = parseFloat(maxDistance); - if (minDistance) formattedValue.$minDistance = parseFloat(minDistance); + if (maxDistance) formattedValue.$maxDistance = parseFloat(maxDistance) + if (minDistance) formattedValue.$minDistance = parseFloat(minDistance) } } if (operator === 'within' || operator === 'intersects') { formattedValue = { $geometry: formattedValue, - }; + } } if (path !== '_id' || (path === '_id' && hasCustomID && field.type === 'text')) { if (operator === 'contains') { - formattedValue = { $regex: formattedValue, $options: 'i' }; + formattedValue = { $options: 'i', $regex: formattedValue } } } if (operator === 'exists') { - formattedValue = (formattedValue === 'true' || formattedValue === true); + formattedValue = formattedValue === 'true' || formattedValue === true } - return formattedValue; -}; + return formattedValue +} diff --git a/packages/db-mongodb/src/queryDrafts.ts b/packages/db-mongodb/src/queryDrafts.ts index 756b87843..f54a4c68f 100644 --- a/packages/db-mongodb/src/queryDrafts.ts +++ b/packages/db-mongodb/src/queryDrafts.ts @@ -1,58 +1,52 @@ -import type { PaginateOptions } from 'mongoose'; -import type { QueryDrafts } from 'payload/database'; -import { flattenWhereToOperators } from 'payload/database'; -import sanitizeInternalFields from './utilities/sanitizeInternalFields'; -import { PayloadRequest } from 'payload/types'; -import type { MongooseAdapter } from '.'; -import { buildSortParam } from './queries/buildSortParam'; -import { withSession } from './withSession'; +import type { PaginateOptions } from 'mongoose' +import type { QueryDrafts } from 'payload/database' +import type { PayloadRequest } from 'payload/types' + +import { flattenWhereToOperators } from 'payload/database' + +import type { MongooseAdapter } from '.' + +import { buildSortParam } from './queries/buildSortParam' +import sanitizeInternalFields from './utilities/sanitizeInternalFields' +import { withSession } from './withSession' type AggregateVersion = { - _id: string; - version: T; - updatedAt: string; - createdAt: string; -}; + _id: string + createdAt: string + updatedAt: string + version: T +} export const queryDrafts: QueryDrafts = async function queryDrafts( this: MongooseAdapter, - { - collection, - where, - page, - limit, - sort: sortArg, - locale, - pagination, - req = {} as PayloadRequest, - }, + { collection, limit, locale, page, pagination, req = {} as PayloadRequest, sort: sortArg, where }, ) { - const VersionModel = this.versions[collection]; - const collectionConfig = this.payload.collections[collection].config; - const options = withSession(this, req.transactionID); + const VersionModel = this.versions[collection] + const collectionConfig = this.payload.collections[collection].config + const options = withSession(this, req.transactionID) const versionQuery = await VersionModel.buildQuery({ - where, locale, payload: this.payload, - }); + where, + }) - let hasNearConstraint = false; + let hasNearConstraint = false if (where) { - const constraints = flattenWhereToOperators(where); - hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')); + const constraints = flattenWhereToOperators(where) + hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) } - let sort; + let sort if (!hasNearConstraint) { sort = buildSortParam({ - sort: sortArg || collectionConfig.defaultSort, - fields: collectionConfig.fields, - timestamps: true, config: this.payload.config, + fields: collectionConfig.fields, locale, - }); + sort: sortArg || collectionConfig.defaultSort, + timestamps: true, + }) } const aggregate = VersionModel.aggregate>( @@ -63,9 +57,9 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( { $group: { _id: '$parent', - version: { $first: '$version' }, - updatedAt: { $first: '$updatedAt' }, createdAt: { $first: '$createdAt' }, + updatedAt: { $first: '$updatedAt' }, + version: { $first: '$version' }, }, }, // Filter based on incoming query @@ -75,43 +69,42 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( ...options, allowDiskUse: true, }, - ); + ) - let result; + let result if (pagination) { - let useEstimatedCount; + let useEstimatedCount if (where) { - const constraints = flattenWhereToOperators(where); - useEstimatedCount = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')); + const constraints = flattenWhereToOperators(where) + useEstimatedCount = constraints.some((prop) => + Object.keys(prop).some((key) => key === 'near'), + ) } const aggregatePaginateOptions: PaginateOptions = { - page, - limit, lean: true, leanWithId: true, - useEstimatedCount, - pagination, - useCustomCountFn: pagination ? undefined : () => Promise.resolve(1), - useFacet: this.connectOptions.useFacet, + limit, options: { ...options, limit, }, + page, + pagination, sort, - }; + useCustomCountFn: pagination ? undefined : () => Promise.resolve(1), + useEstimatedCount, + useFacet: this.connectOptions.useFacet, + } - result = await VersionModel.aggregatePaginate( - aggregate, - aggregatePaginateOptions, - ); + result = await VersionModel.aggregatePaginate(aggregate, aggregatePaginateOptions) } else { - result = aggregate.exec(); + result = aggregate.exec() } - const docs = JSON.parse(JSON.stringify(result.docs)); + const docs = JSON.parse(JSON.stringify(result.docs)) return { ...result, @@ -121,11 +114,11 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( _id: doc._id, id: doc._id, ...doc.version, - updatedAt: doc.updatedAt, createdAt: doc.createdAt, - }; + updatedAt: doc.updatedAt, + } - return sanitizeInternalFields(doc); + return sanitizeInternalFields(doc) }), - }; -}; + } +} diff --git a/packages/db-mongodb/src/testCredentials.ts b/packages/db-mongodb/src/testCredentials.ts index 86a943821..ac966c89b 100644 --- a/packages/db-mongodb/src/testCredentials.ts +++ b/packages/db-mongodb/src/testCredentials.ts @@ -1,7 +1,7 @@ -export const email = 'test@test.com'; -export const password = 'test123'; +export const email = 'test@test.com' +export const password = 'test123' export const connection = { - url: 'mongodb://127.0.0.1', - port: 27018, name: 'payloadmemory', -}; + port: 27018, + url: 'mongodb://127.0.0.1', +} diff --git a/packages/db-mongodb/src/transactions/beginTransaction.ts b/packages/db-mongodb/src/transactions/beginTransaction.ts index 2ba560611..2941389a8 100644 --- a/packages/db-mongodb/src/transactions/beginTransaction.ts +++ b/packages/db-mongodb/src/transactions/beginTransaction.ts @@ -1,32 +1,35 @@ -import type { TransactionOptions } from 'mongodb'; -import { v4 as uuid } from 'uuid'; -import { BeginTransaction } from 'payload/database'; -import { APIError } from 'payload/errors'; +import type { TransactionOptions } from 'mongodb' +import type { BeginTransaction } from 'payload/database' -let transactionsNotAvailable: boolean; +import { APIError } from 'payload/errors' +import { v4 as uuid } from 'uuid' + +let transactionsNotAvailable: boolean export const beginTransaction: BeginTransaction = async function beginTransaction( options: TransactionOptions = {}, ) { - let id = null; + let id = null if (!this.connection) { - throw new APIError('beginTransaction called while no connection to the database exists'); + throw new APIError('beginTransaction called while no connection to the database exists') } - if (transactionsNotAvailable) return id; + if (transactionsNotAvailable) return id if (!this.connection.get('replicaSet')) { - transactionsNotAvailable = true; - this.payload.logger.warn('Database transactions for MongoDB are only available when connecting to a replica set. Operations will continue without using transactions.'); + transactionsNotAvailable = true + this.payload.logger.warn( + 'Database transactions for MongoDB are only available when connecting to a replica set. Operations will continue without using transactions.', + ) } else { - id = uuid(); + id = uuid() if (!this.sessions[id]) { - this.sessions[id] = await this.connection.getClient().startSession(); + this.sessions[id] = await this.connection.getClient().startSession() } if (this.sessions[id].inTransaction()) { - this.payload.logger.warn('beginTransaction called while transaction already exists'); + this.payload.logger.warn('beginTransaction called while transaction already exists') } else { - await this.sessions[id].startTransaction(options); + await this.sessions[id].startTransaction(options) } } - return id; -}; + return id +} diff --git a/packages/db-mongodb/src/transactions/commitTransaction.ts b/packages/db-mongodb/src/transactions/commitTransaction.ts index 5fe98aaef..43869c3d9 100644 --- a/packages/db-mongodb/src/transactions/commitTransaction.ts +++ b/packages/db-mongodb/src/transactions/commitTransaction.ts @@ -1,15 +1,14 @@ -import { CommitTransaction } from 'payload/database'; - +import type { CommitTransaction } from 'payload/database' export const commitTransaction: CommitTransaction = async function commitTransaction(id) { if (!this.connection.get('replicaSet')) { - return; + return } if (!this.session[id]?.inTransaction()) { - this.payload.logger.warn('commitTransaction called when no transaction exists'); - return; + this.payload.logger.warn('commitTransaction called when no transaction exists') + return } - await this.session[id].commitTransaction(); - await this.session[id].endSession(); - delete this.session[id]; -}; + await this.session[id].commitTransaction() + await this.session[id].endSession() + delete this.session[id] +} diff --git a/packages/db-mongodb/src/transactions/rollbackTransaction.ts b/packages/db-mongodb/src/transactions/rollbackTransaction.ts index a8952f0a5..ec30a865e 100644 --- a/packages/db-mongodb/src/transactions/rollbackTransaction.ts +++ b/packages/db-mongodb/src/transactions/rollbackTransaction.ts @@ -1,12 +1,13 @@ -import { RollbackTransaction } from 'payload/database'; +import type { RollbackTransaction } from 'payload/database' - -export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction(id = '') { +export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction( + id = '', +) { if (!this.session[id]?.inTransaction()) { - this.payload.logger.warn('rollbackTransaction called when no transaction exists'); - return; + this.payload.logger.warn('rollbackTransaction called when no transaction exists') + return } - await this.session[id].abortTransaction(); - await this.session[id].endSession(); - delete this.session[id]; -}; + await this.session[id].abortTransaction() + await this.session[id].endSession() + delete this.session[id] +} diff --git a/packages/db-mongodb/src/types.ts b/packages/db-mongodb/src/types.ts index fdf417324..66a06fa9c 100644 --- a/packages/db-mongodb/src/types.ts +++ b/packages/db-mongodb/src/types.ts @@ -1,23 +1,56 @@ -import type { AggregatePaginateModel, IndexDefinition, IndexOptions, Model, PaginateModel, SchemaOptions } from 'mongoose'; -import { SanitizedConfig } from 'payload/config'; -import { ArrayField, BlockField, CheckboxField, CodeField, CollapsibleField, DateField, EmailField, Field, GroupField, JSONField, NumberField, PointField, RadioField, RelationshipField, RichTextField, RowField, SelectField, TabsField, TextField, TextareaField, UploadField } from 'payload/types'; -import type { BuildQueryArgs } from './queries/buildQuery'; +import type { + AggregatePaginateModel, + IndexDefinition, + IndexOptions, + Model, + PaginateModel, + SchemaOptions, +} from 'mongoose' +import type { SanitizedConfig } from 'payload/config' +import type { + ArrayField, + BlockField, + CheckboxField, + CodeField, + CollapsibleField, + DateField, + EmailField, + Field, + GroupField, + JSONField, + NumberField, + PointField, + RadioField, + RelationshipField, + RichTextField, + RowField, + SelectField, + TabsField, + TextField, + TextareaField, + UploadField, +} from 'payload/types' -export interface CollectionModel extends Model, PaginateModel, AggregatePaginateModel, PassportLocalModel { +import type { BuildQueryArgs } from './queries/buildQuery' + +export interface CollectionModel + extends Model, + PaginateModel, + AggregatePaginateModel, + PassportLocalModel { /** buildQuery is used to transform payload's where operator into what can be used by mongoose (e.g. id => _id) */ buildQuery: (args: BuildQueryArgs) => Promise> // TODO: Delete this } -type Register = (doc: T, password: string) => T; +type Register = (doc: T, password: string) => T interface PassportLocalModel { - register: Register authenticate: any + register: Register } - export interface AuthCollectionModel extends CollectionModel { - resetPasswordToken: string; - resetPasswordExpiration: Date; + resetPasswordExpiration: Date + resetPasswordToken: string } export type TypeOfIndex = { @@ -25,80 +58,82 @@ export type TypeOfIndex = { options?: IndexOptions } - export interface GlobalModel extends Model { buildQuery: (query: unknown, locale?: string) => Promise> } export type BuildSchema = (args: { - config: SanitizedConfig, - fields: Field[], - options: BuildSchemaOptions, + config: SanitizedConfig + fields: Field[] + options: BuildSchemaOptions }) => TSchema export type BuildSchemaOptions = { - options?: SchemaOptions allowIDField?: boolean disableUnique?: boolean draftsEnabled?: boolean indexSortableFields?: boolean + options?: SchemaOptions } export type FieldGenerator = { - field: TField, - schema: TSchema, - config: SanitizedConfig, - options: BuildSchemaOptions, + config: SanitizedConfig + field: TField + options: BuildSchemaOptions + schema: TSchema } /** * Field config types that need representation in the database */ -type FieldType = 'number' - | 'text' - | 'email' - | 'textarea' - | 'richText' +type FieldType = + | 'array' + | 'blocks' + | 'checkbox' | 'code' + | 'collapsible' + | 'date' + | 'email' + | 'group' | 'json' + | 'number' | 'point' | 'radio' - | 'checkbox' - | 'date' - | 'upload' | 'relationship' + | 'richText' | 'row' - | 'collapsible' - | 'tabs' - | 'array' - | 'group' | 'select' - | 'blocks' + | 'tabs' + | 'text' + | 'textarea' + | 'upload' -export type FieldGeneratorFunction = (args: FieldGenerator) => void +export type FieldGeneratorFunction = ( + args: FieldGenerator, +) => void /** * Object mapping types to a schema based on TSchema */ export type FieldToSchemaMap = { - number: FieldGeneratorFunction - text: FieldGeneratorFunction - email: FieldGeneratorFunction - textarea: FieldGeneratorFunction - richText: FieldGeneratorFunction + array: FieldGeneratorFunction + blocks: FieldGeneratorFunction + checkbox: FieldGeneratorFunction code: FieldGeneratorFunction + collapsible: FieldGeneratorFunction + date: FieldGeneratorFunction + email: FieldGeneratorFunction + group: FieldGeneratorFunction json: FieldGeneratorFunction + number: FieldGeneratorFunction point: FieldGeneratorFunction radio: FieldGeneratorFunction - checkbox: FieldGeneratorFunction - date: FieldGeneratorFunction - upload: FieldGeneratorFunction relationship: FieldGeneratorFunction + richText: FieldGeneratorFunction row: FieldGeneratorFunction - collapsible: FieldGeneratorFunction - tabs: FieldGeneratorFunction - array: FieldGeneratorFunction - group: FieldGeneratorFunction select: FieldGeneratorFunction - blocks: FieldGeneratorFunction + tabs: FieldGeneratorFunction + text: FieldGeneratorFunction + textarea: FieldGeneratorFunction + upload: FieldGeneratorFunction } diff --git a/packages/db-mongodb/src/updateGlobal.ts b/packages/db-mongodb/src/updateGlobal.ts index 0a8e4ff5b..f26af9152 100644 --- a/packages/db-mongodb/src/updateGlobal.ts +++ b/packages/db-mongodb/src/updateGlobal.ts @@ -1,32 +1,30 @@ -import type { UpdateGlobal } from 'payload/database'; -import sanitizeInternalFields from './utilities/sanitizeInternalFields'; -import type { PayloadRequest } from 'payload/types'; -import type { MongooseAdapter } from '.'; -import { withSession } from './withSession'; +import type { UpdateGlobal } from 'payload/database' +import type { PayloadRequest } from 'payload/types' + +import type { MongooseAdapter } from '.' + +import sanitizeInternalFields from './utilities/sanitizeInternalFields' +import { withSession } from './withSession' export const updateGlobal: UpdateGlobal = async function updateGlobal( this: MongooseAdapter, - { slug, data, req = {} as PayloadRequest }, + { data, req = {} as PayloadRequest, slug }, ) { - const Model = this.globals; + const Model = this.globals const options = { ...withSession(this, req.transactionID), - new: true, lean: true, - }; + new: true, + } - let result; - result = await Model.findOneAndUpdate( - { globalType: slug }, - data, - options, - ); + let result + result = await Model.findOneAndUpdate({ globalType: slug }, data, options) - result = JSON.parse(JSON.stringify(result)); + result = JSON.parse(JSON.stringify(result)) // custom id type reset - result.id = result._id; - result = sanitizeInternalFields(result); + result.id = result._id + result = sanitizeInternalFields(result) - return result; -}; + return result +} diff --git a/packages/db-mongodb/src/updateOne.ts b/packages/db-mongodb/src/updateOne.ts index 0e9d04abe..b855f71b9 100644 --- a/packages/db-mongodb/src/updateOne.ts +++ b/packages/db-mongodb/src/updateOne.ts @@ -1,50 +1,53 @@ -import { ValidationError } from 'payload/errors'; -import type { PayloadRequest } from 'payload/types'; -import type { UpdateOne } from 'payload/database'; -import { i18nInit } from 'payload/utilities'; -import sanitizeInternalFields from './utilities/sanitizeInternalFields'; -import type { MongooseAdapter } from '.'; -import { withSession } from './withSession'; +import type { UpdateOne } from 'payload/database' +import type { PayloadRequest } from 'payload/types' + +import { ValidationError } from 'payload/errors' +import { i18nInit } from 'payload/utilities' + +import type { MongooseAdapter } from '.' + +import sanitizeInternalFields from './utilities/sanitizeInternalFields' +import { withSession } from './withSession' export const updateOne: UpdateOne = async function updateOne( this: MongooseAdapter, - { collection, data, where: whereArg, id, locale, req = {} as PayloadRequest }, + { collection, data, id, locale, req = {} as PayloadRequest, where: whereArg }, ) { - const where = id ? { id: { equals: id } } : whereArg; - const Model = this.collections[collection]; + const where = id ? { id: { equals: id } } : whereArg + const Model = this.collections[collection] const options = { ...withSession(this, req.transactionID), - new: true, lean: true, - }; + new: true, + } const query = await Model.buildQuery({ - payload: this.payload, locale, + payload: this.payload, where, - }); + }) - let result; + let result try { - result = await Model.findOneAndUpdate(query, data, options); + result = await Model.findOneAndUpdate(query, data, options) } catch (error) { // Handle uniqueness error from MongoDB throw error.code === 11000 && error.keyValue ? new ValidationError( - [ - { - message: 'Value must be unique', - field: Object.keys(error.keyValue)[0], - }, - ], - req?.t ?? i18nInit(this.payload.config.i18n).t, - ) - : error; + [ + { + field: Object.keys(error.keyValue)[0], + message: 'Value must be unique', + }, + ], + req?.t ?? i18nInit(this.payload.config.i18n).t, + ) + : error } - result = JSON.parse(JSON.stringify(result)); - result.id = result._id; - result = sanitizeInternalFields(result); + result = JSON.parse(JSON.stringify(result)) + result.id = result._id + result = sanitizeInternalFields(result) - return result; -}; + return result +} diff --git a/packages/db-mongodb/src/updateVersion.ts b/packages/db-mongodb/src/updateVersion.ts index 6be754032..b24c6dd3e 100644 --- a/packages/db-mongodb/src/updateVersion.ts +++ b/packages/db-mongodb/src/updateVersion.ts @@ -1,35 +1,37 @@ -import type { UpdateVersion } from 'payload/database'; -import type { PayloadRequest } from 'payload/types'; -import type { MongooseAdapter } from '.'; -import { withSession } from './withSession'; +import type { UpdateVersion } from 'payload/database' +import type { PayloadRequest } from 'payload/types' + +import type { MongooseAdapter } from '.' + +import { withSession } from './withSession' export const updateVersion: UpdateVersion = async function updateVersion( this: MongooseAdapter, - { collectionSlug, where, locale, versionData, req = {} as PayloadRequest }, + { collectionSlug, locale, req = {} as PayloadRequest, versionData, where }, ) { - const VersionModel = this.versions[collectionSlug]; + const VersionModel = this.versions[collectionSlug] const options = { ...withSession(this, req.transactionID), - new: true, lean: true, - }; + new: true, + } const query = await VersionModel.buildQuery({ - payload: this.payload, locale, + payload: this.payload, where, - }); + }) - const doc = await VersionModel.findOneAndUpdate(query, versionData, options); + const doc = await VersionModel.findOneAndUpdate(query, versionData, options) - const result = JSON.parse(JSON.stringify(doc)); + const result = JSON.parse(JSON.stringify(doc)) - const verificationToken = doc._verificationToken; + const verificationToken = doc._verificationToken // custom id type reset - result.id = result._id; + result.id = result._id if (verificationToken) { - result._verificationToken = verificationToken; + result._verificationToken = verificationToken } - return result; -}; + return result +} diff --git a/packages/db-mongodb/src/utilities/createArrayFromCommaDelineated.ts b/packages/db-mongodb/src/utilities/createArrayFromCommaDelineated.ts index 90949cf75..344b59898 100644 --- a/packages/db-mongodb/src/utilities/createArrayFromCommaDelineated.ts +++ b/packages/db-mongodb/src/utilities/createArrayFromCommaDelineated.ts @@ -1,7 +1,7 @@ export function createArrayFromCommaDelineated(input: string): string[] { - if (Array.isArray(input)) return input; + if (Array.isArray(input)) return input if (input.indexOf(',') > -1) { - return input.split(','); + return input.split(',') } - return [input]; + return [input] } diff --git a/packages/db-mongodb/src/utilities/sanitizeInternalFields.ts b/packages/db-mongodb/src/utilities/sanitizeInternalFields.ts index 66f3d8e47..6877cde86 100644 --- a/packages/db-mongodb/src/utilities/sanitizeInternalFields.ts +++ b/packages/db-mongodb/src/utilities/sanitizeInternalFields.ts @@ -1,21 +1,22 @@ -const internalFields = ['__v']; +const internalFields = ['__v'] + +const sanitizeInternalFields = >(incomingDoc: T): T => + Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => { + if (key === '_id') { + return { + ...newDoc, + id: val, + } + } + + if (internalFields.indexOf(key) > -1) { + return newDoc + } -const sanitizeInternalFields = >(incomingDoc: T): T => Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => { - if (key === '_id') { return { ...newDoc, - id: val, - }; - } + [key]: val, + } + }, {} as T) - if (internalFields.indexOf(key) > -1) { - return newDoc; - } - - return { - ...newDoc, - [key]: val, - }; -}, {} as T); - -export default sanitizeInternalFields; +export default sanitizeInternalFields diff --git a/packages/db-mongodb/src/webpack.ts b/packages/db-mongodb/src/webpack.ts index 17f8a371e..e60d64609 100644 --- a/packages/db-mongodb/src/webpack.ts +++ b/packages/db-mongodb/src/webpack.ts @@ -1,15 +1,16 @@ -import path from 'path'; -import type { Webpack } from 'payload/database'; +import type { Webpack } from 'payload/database' + +import path from 'path' export const webpack: Webpack = (config) => { return { ...config, resolve: { - ...config.resolve || {}, + ...(config.resolve || {}), alias: { - ...config.resolve?.alias || {}, + ...(config.resolve?.alias || {}), [path.resolve(__dirname, './index')]: path.resolve(__dirname, 'mock'), }, }, - }; -}; + } +} diff --git a/packages/db-mongodb/src/withSession.ts b/packages/db-mongodb/src/withSession.ts index 2c957961c..d7aff267f 100644 --- a/packages/db-mongodb/src/withSession.ts +++ b/packages/db-mongodb/src/withSession.ts @@ -1,10 +1,14 @@ -import type { ClientSession } from 'mongoose'; -import { MongooseAdapter } from './index'; +import type { ClientSession } from 'mongoose' + +import type { MongooseAdapter } from './index' /** * returns the session belonging to the transaction of the req.session if exists * @returns ClientSession */ -export function withSession(db: MongooseAdapter, transactionID?: string | number): { session: ClientSession } | object { - return db.sessions[transactionID] ? { session: db.sessions[transactionID] } : {}; +export function withSession( + db: MongooseAdapter, + transactionID?: number | string, +): { session: ClientSession } | object { + return db.sessions[transactionID] ? { session: db.sessions[transactionID] } : {} } diff --git a/packages/db-mongodb/tsconfig.json b/packages/db-mongodb/tsconfig.json index 8f6573be7..27b3e1694 100644 --- a/packages/db-mongodb/tsconfig.json +++ b/packages/db-mongodb/tsconfig.json @@ -1,16 +1,16 @@ { "compilerOptions": { - "target": "ESNext", - "module": "NodeNext", - "moduleResolution": "NodeNext" /* Required for exports to work */, "composite": true, "declaration": true /* Generates corresponding '.d.ts' file. */, - "rootDir": "./src" /* Specify the root folder within your source files. */, - "outDir": "./dist" /* Specify an output folder for all emitted files. */, "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + "module": "NodeNext", + "moduleResolution": "NodeNext" /* Required for exports to work */, + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + "resolveJsonModule": true, + "rootDir": "./src" /* Specify the root folder within your source files. */, "skipLibCheck": true /* Skip type checking all .d.ts files. */, - "resolveJsonModule": true + "target": "ESNext" }, "exclude": [ "dist", diff --git a/packages/db-postgres/.eslintrc.cjs b/packages/db-postgres/.eslintrc.cjs index 6a2f2ade0..638d7f813 100644 --- a/packages/db-postgres/.eslintrc.cjs +++ b/packages/db-postgres/.eslintrc.cjs @@ -4,7 +4,7 @@ module.exports = { overrides: [ { extends: ['plugin:@typescript-eslint/disable-type-checked'], - files: ['*.js', '*.cjs'], + files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'], }, ], parserOptions: { diff --git a/packages/db-postgres/package.json b/packages/db-postgres/package.json index 388b6cabe..276d7203b 100644 --- a/packages/db-postgres/package.json +++ b/packages/db-postgres/package.json @@ -1,41 +1,5 @@ { - "name": "@payloadcms/db-postgres", - "version": "0.0.1", - "description": "The officially supported Postgres database adapter for Payload", - "main": "./src/index.ts", - "types": "./src/index.ts", - "publishConfig": { - "registry": "https://registry.npmjs.org/", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js", - "node": "./dist/index.js", - "require": "./dist/index.js" - } - } - }, - "exports": { - ".": { - "types": "./src/index.ts", - "import": "./src/index.ts", - "default": "./src/index.ts", - "node": "./src/index.ts", - "require": "./src/index.ts" - } - }, - "repository": "https://github.com/payloadcms/payload", "author": "Payload CMS, Inc.", - "license": "MIT", - "scripts": { - "builddisabled": "tsc" - }, - "peerDependencies": { - "better-sqlite3": "^8.5.0" - }, "dependencies": { "@libsql/client": "^0.3.1", "drizzle-kit": "0.19.13-e99bac1", @@ -44,11 +8,47 @@ "prompts": "2.4.2", "to-snake-case": "1.0.0" }, + "description": "The officially supported Postgres database adapter for Payload", "devDependencies": { + "@payloadcms/eslint-config": "workspace:*", "@types/pg": "8.10.2", "@types/to-snake-case": "1.0.0", "better-sqlite3": "^8.5.0", - "payload": "workspace:*", - "@payloadcms/eslint-config": "workspace:*" - } + "payload": "workspace:*" + }, + "exports": { + ".": { + "default": "./src/index.ts", + "import": "./src/index.ts", + "node": "./src/index.ts", + "require": "./src/index.ts", + "types": "./src/index.ts" + } + }, + "license": "MIT", + "main": "./src/index.ts", + "name": "@payloadcms/db-postgres", + "peerDependencies": { + "better-sqlite3": "^8.5.0" + }, + "publishConfig": { + "exports": { + ".": { + "default": "./dist/index.js", + "import": "./dist/index.js", + "node": "./dist/index.js", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "main": "./dist/index.js", + "registry": "https://registry.npmjs.org/", + "types": "./dist/index.d.ts" + }, + "repository": "https://github.com/payloadcms/payload", + "scripts": { + "builddisabled": "tsc" + }, + "types": "./src/index.ts", + "version": "0.0.1" } diff --git a/packages/db-postgres/src/connect.ts b/packages/db-postgres/src/connect.ts index f20b4ddd7..4b97bceef 100644 --- a/packages/db-postgres/src/connect.ts +++ b/packages/db-postgres/src/connect.ts @@ -1,145 +1,147 @@ -import fs from 'fs'; -import { sql, eq } from 'drizzle-orm'; -import { drizzle } from 'drizzle-orm/node-postgres'; -import type { Connect } from 'payload/database'; -import { Client, Pool } from 'pg'; -import { generateDrizzleJson, pushSchema } from 'drizzle-kit/utils'; -import { configToJSONSchema } from 'payload/utilities'; -import prompts from 'prompts'; +import type { Connect } from 'payload/database' -import { jsonb, numeric, pgTable, varchar } from 'drizzle-orm/pg-core'; -import type { PostgresAdapter } from './types'; -import { DrizzleDB, GenericEnum, GenericRelation, GenericTable } from './types'; +import { generateDrizzleJson, pushSchema } from 'drizzle-kit/utils' +import { eq, sql } from 'drizzle-orm' +import { drizzle } from 'drizzle-orm/node-postgres' +import { jsonb, numeric, pgTable, varchar } from 'drizzle-orm/pg-core' +import fs from 'fs' +import { configToJSONSchema } from 'payload/utilities' +import { Client, Pool } from 'pg' +import prompts from 'prompts' + +import type { DrizzleDB, PostgresAdapter } from './types' + +import { GenericEnum, GenericRelation, GenericTable } from './types' // Migration table def in order to use query using drizzle const migrationsSchema = pgTable('payload_migrations', { - name: varchar('name'), batch: numeric('batch'), + name: varchar('name'), schema: jsonb('schema'), -}); +}) -export const connect: Connect = async function connect( - this: PostgresAdapter, - payload, -) { - let db: DrizzleDB; +export const connect: Connect = async function connect(this: PostgresAdapter, payload) { + let db: DrizzleDB this.schema = { ...this.tables, ...this.relations, ...this.enums, - }; + } try { if ('pool' in this && this.pool !== false) { - const pool = new Pool(this.pool); - db = drizzle(pool, { schema: this.schema }); - await pool.connect(); + const pool = new Pool(this.pool) + db = drizzle(pool, { schema: this.schema }) + await pool.connect() } if ('client' in this && this.client !== false) { - const client = new Client(this.client); - db = drizzle(client, { schema: this.schema }); - await client.connect(); + const client = new Client(this.client) + db = drizzle(client, { schema: this.schema }) + await client.connect() } if (process.env.PAYLOAD_DROP_DATABASE === 'true') { - this.payload.logger.info('---- DROPPING TABLES ----'); - await db.execute(sql`drop schema public cascade;\ncreate schema public;`); - this.payload.logger.info('---- DROPPED TABLES ----'); + this.payload.logger.info('---- DROPPING TABLES ----') + await db.execute(sql`drop schema public cascade;\ncreate schema public;`) + this.payload.logger.info('---- DROPPED TABLES ----') } } catch (err) { - payload.logger.error( - `Error: cannot connect to Postgres. Details: ${err.message}`, - err, - ); - process.exit(1); + payload.logger.error(`Error: cannot connect to Postgres. Details: ${err.message}`, err) + process.exit(1) } - this.payload.logger.info('Connected to Postgres successfully'); - this.db = db; + this.payload.logger.info('Connected to Postgres successfully') + this.db = db // Only push schema if not in production - if (process.env.NODE_ENV === 'production') return; + if (process.env.NODE_ENV === 'production') return // This will prompt if clarifications are needed for Drizzle to push new schema - const { hasDataLoss, warnings, statementsToExecute, apply } = await pushSchema(this.schema, this.db); + const { apply, hasDataLoss, statementsToExecute, warnings } = await pushSchema( + this.schema, + this.db, + ) this.payload.logger.debug({ - msg: 'Schema push results', hasDataLoss, - warnings, + msg: 'Schema push results', statementsToExecute, - }); + warnings, + }) if (warnings.length) { this.payload.logger.warn({ msg: `Warnings detected during schema push: ${warnings.join('\n')}`, warnings, - }); + }) if (hasDataLoss) { this.payload.logger.warn({ msg: 'DATA LOSS WARNING: Possible data loss detected if schema is pushed.', - }); + }) } const { confirm: acceptWarnings } = await prompts( { - type: 'confirm', - name: 'confirm', - message: 'Accept warnings and push schema to database?', initial: false, + message: 'Accept warnings and push schema to database?', + name: 'confirm', + type: 'confirm', }, { onCancel: () => { - process.exit(0); + process.exit(0) }, }, - ); + ) // Exit if user does not accept warnings. // Q: Is this the right type of exit for this interaction? if (!acceptWarnings) { - process.exit(0); + process.exit(0) } } - this.migrationDir = '.migrations'; + this.migrationDir = '.migrations' // Create drizzle snapshot if it doesn't exist if (!fs.existsSync(`${this.migrationDir}/drizzle-snapshot.json`)) { // Ensure migration dir exists if (!fs.existsSync(this.migrationDir)) { - fs.mkdirSync(this.migrationDir); + fs.mkdirSync(this.migrationDir) } - const drizzleJSON = generateDrizzleJson(this.schema); + const drizzleJSON = generateDrizzleJson(this.schema) - fs.writeFileSync(`${this.migrationDir}/drizzle-snapshot.json`, JSON.stringify(drizzleJSON, null, 2)); + fs.writeFileSync( + `${this.migrationDir}/drizzle-snapshot.json`, + JSON.stringify(drizzleJSON, null, 2), + ) } - const jsonSchema = configToJSONSchema(this.payload.config); + const jsonSchema = configToJSONSchema(this.payload.config) - await apply(); + await apply() const devPush = await this.db .select() .from(migrationsSchema) - .where(eq(migrationsSchema.batch, '-1')); + .where(eq(migrationsSchema.batch, '-1')) if (!devPush.length) { await this.db.insert(migrationsSchema).values({ - name: 'dev', batch: '-1', + name: 'dev', schema: JSON.stringify(jsonSchema), - }); + }) } else { await this.db .update(migrationsSchema) .set({ schema: JSON.stringify(jsonSchema), }) - .where(eq(migrationsSchema.batch, '-1')); + .where(eq(migrationsSchema.batch, '-1')) } -}; +} diff --git a/packages/db-postgres/src/create/index.ts b/packages/db-postgres/src/create/index.ts index 6828990af..ab7bd5829 100644 --- a/packages/db-postgres/src/create/index.ts +++ b/packages/db-postgres/src/create/index.ts @@ -1,13 +1,11 @@ -import { Create } from 'payload/database'; -import toSnakeCase from 'to-snake-case'; -import { upsertRow } from '../upsertRow'; +import type { Create } from 'payload/database' -export const create: Create = async function create({ - collection: collectionSlug, - data, - req, -}) { - const collection = this.payload.collections[collectionSlug].config; +import toSnakeCase from 'to-snake-case' + +import { upsertRow } from '../upsertRow' + +export const create: Create = async function create({ collection: collectionSlug, data, req }) { + const collection = this.payload.collections[collectionSlug].config const result = await upsertRow({ adapter: this, @@ -17,7 +15,7 @@ export const create: Create = async function create({ locale: req.locale, operation: 'create', tableName: toSnakeCase(collectionSlug), - }); + }) - return result; -}; + return result +} diff --git a/packages/db-postgres/src/createMigration.ts b/packages/db-postgres/src/createMigration.ts index 2ee83ec59..48997924a 100644 --- a/packages/db-postgres/src/createMigration.ts +++ b/packages/db-postgres/src/createMigration.ts @@ -1,16 +1,18 @@ /* eslint-disable no-restricted-syntax, no-await-in-loop */ -import fs from 'fs'; -import { CreateMigration } from 'payload/database'; +import type { CreateMigration } from 'payload/database' +import type { DatabaseAdapter, Init } from 'payload/database' -import { generateDrizzleJson, generateMigration } from 'drizzle-kit/utils'; -import { eq } from 'drizzle-orm'; -import { jsonb, numeric, pgEnum, pgTable, varchar } from 'drizzle-orm/pg-core'; -import { SanitizedCollectionConfig } from 'payload/types'; -import type { DatabaseAdapter, Init } from 'payload/database'; -import { configToJSONSchema } from 'payload/utilities'; -import prompts from 'prompts'; -import { buildTable } from './schema/build'; -import type { GenericEnum, GenericRelation, GenericTable, PostgresAdapter } from './types'; +import { generateDrizzleJson, generateMigration } from 'drizzle-kit/utils' +import { eq } from 'drizzle-orm' +import { jsonb, numeric, pgEnum, pgTable, varchar } from 'drizzle-orm/pg-core' +import fs from 'fs' +import { SanitizedCollectionConfig } from 'payload/types' +import { configToJSONSchema } from 'payload/utilities' +import prompts from 'prompts' + +import type { GenericEnum, GenericRelation, GenericTable, PostgresAdapter } from './types' + +import { buildTable } from './schema/build' const migrationTemplate = (upSQL?: string) => ` import payload, { Payload } from 'payload'; @@ -22,7 +24,7 @@ export async function up(payload: Payload): Promise { export async function down(payload: Payload): Promise { // Migration code }; -`; +` export const createMigration: CreateMigration = async function createMigration( this: PostgresAdapter, @@ -30,27 +32,30 @@ export const createMigration: CreateMigration = async function createMigration( migrationDir, migrationName, ) { - payload.logger.info({ msg: 'Creating migration from postgres adapter...' }); - const dir = migrationDir || '.migrations'; // TODO: Verify path after linking + payload.logger.info({ msg: 'Creating migration from postgres adapter...' }) + const dir = migrationDir || '.migrations' // TODO: Verify path after linking if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); + fs.mkdirSync(dir) } - const [yyymmdd, hhmmss] = new Date().toISOString().split('T'); - const formattedDate = yyymmdd.replace(/\D/g, ''); - const formattedTime = hhmmss.split('.')[0].replace(/\D/g, ''); + const [yyymmdd, hhmmss] = new Date().toISOString().split('T') + const formattedDate = yyymmdd.replace(/\D/g, '') + const formattedTime = hhmmss.split('.')[0].replace(/\D/g, '') - const timestamp = `${formattedDate}_${formattedTime}`; + const timestamp = `${formattedDate}_${formattedTime}` - const formattedName = migrationName.replace(/\W/g, '_'); - const fileName = `${timestamp}_${formattedName}.ts`; - const filePath = `${dir}/${fileName}`; + const formattedName = migrationName.replace(/\W/g, '_') + const fileName = `${timestamp}_${formattedName}.ts` + const filePath = `${dir}/${fileName}` - const snapshotJSON = fs.readFileSync(`${dir}/drizzle-snapshot.json`, 'utf8'); - const drizzleJsonBefore = generateDrizzleJson(JSON.parse(snapshotJSON)); - const drizzleJsonAfter = generateDrizzleJson(this.schema, drizzleJsonBefore.id); - const sqlStatements = await generateMigration(drizzleJsonBefore, drizzleJsonAfter); - fs.writeFileSync(filePath, migrationTemplate(sqlStatements.length ? sqlStatements?.join('\n') : undefined)); + const snapshotJSON = fs.readFileSync(`${dir}/drizzle-snapshot.json`, 'utf8') + const drizzleJsonBefore = generateDrizzleJson(JSON.parse(snapshotJSON)) + const drizzleJsonAfter = generateDrizzleJson(this.schema, drizzleJsonBefore.id) + const sqlStatements = await generateMigration(drizzleJsonBefore, drizzleJsonAfter) + fs.writeFileSync( + filePath, + migrationTemplate(sqlStatements.length ? sqlStatements?.join('\n') : undefined), + ) // TODO: // Get the most recent migration schema from the file system @@ -61,4 +66,4 @@ export const createMigration: CreateMigration = async function createMigration( // and then inject them each into the `migrationTemplate` above, // outputting the file into the migrations folder accordingly // also make sure to output the JSON schema snapshot into a `./migrationsDir/meta` folder like Drizzle does -}; +} diff --git a/packages/db-postgres/src/find.ts b/packages/db-postgres/src/find.ts index d45701b92..1d7a3c9b7 100644 --- a/packages/db-postgres/src/find.ts +++ b/packages/db-postgres/src/find.ts @@ -1,63 +1,70 @@ -import { sql } from 'drizzle-orm'; -import toSnakeCase from 'to-snake-case'; -import type { Find } from 'payload/database'; -import type { PayloadRequest } from 'payload/types'; -import type { SanitizedCollectionConfig } from 'payload/types'; -import buildQuery from './queries/buildQuery'; +import type { Find } from 'payload/database' +import type { SanitizedCollectionConfig } from 'payload/types' +import type { PayloadRequest } from 'payload/types' + +import { sql } from 'drizzle-orm' +import toSnakeCase from 'to-snake-case' + +import buildQuery from './queries/buildQuery' export const find: Find = async function find({ collection, - where, - page = 1, limit: limitArg, - sort: sortArg, locale, + page = 1, pagination, req = {} as PayloadRequest, + sort: sortArg, + where, }) { - const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config; - const tableName = toSnakeCase(collection); - const table = this.tables[tableName]; - const limit = typeof limitArg === 'number' ? limitArg : collectionConfig.admin.pagination.defaultLimit; - const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort; - let totalDocs; - let totalPages; - let hasPrevPage; - let hasNextPage; - let pagingCounter; + const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config + const tableName = toSnakeCase(collection) + const table = this.tables[tableName] + const limit = + typeof limitArg === 'number' ? limitArg : collectionConfig.admin.pagination.defaultLimit + const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort + let totalDocs + let totalPages + let hasPrevPage + let hasNextPage + let pagingCounter const query = await buildQuery({ - collectionSlug: collection, adapter: this, + collectionSlug: collection, locale, where, - }); + }) if (pagination !== false) { - const countResult = await this.db.select({ count: sql`count(*)` }).from(table).where(query); - totalDocs = Number(countResult[0].count); - totalPages = Math.ceil(totalDocs / limit); - hasPrevPage = page > 1; - hasNextPage = totalPages > page; - pagingCounter = ((page - 1) * limit) + 1; + const countResult = await this.db + .select({ count: sql`count(*)` }) + .from(table) + .where(query) + totalDocs = Number(countResult[0].count) + totalPages = Math.ceil(totalDocs / limit) + hasPrevPage = page > 1 + hasNextPage = totalPages > page + pagingCounter = (page - 1) * limit + 1 } - const docs = await this.db.select() + const docs = await this.db + .select() .from(table) .limit(limit === 0 ? undefined : limit) .offset((page - 1) * limit) - .where(query); + .where(query) return { docs, // : T[] - totalDocs, // : number + hasNextPage, // : boolean + hasPrevPage, // : boolean limit, // : number - totalPages, // : number + nextPage: hasNextPage ? page + 1 : null, // ?: number | null | undefined page, // ?: number pagingCounter, // : number - hasPrevPage, // : boolean - hasNextPage, // : boolean prevPage: hasPrevPage ? page - 1 : null, // ?: number | null | undefined - nextPage: hasNextPage ? page + 1 : null, // ?: number | null | undefined - }; -}; + totalDocs, // : number + totalPages, // : number + } +} diff --git a/packages/db-postgres/src/find/buildFindManyArgs.ts b/packages/db-postgres/src/find/buildFindManyArgs.ts index 656f02a5b..7f1c8fb57 100644 --- a/packages/db-postgres/src/find/buildFindManyArgs.ts +++ b/packages/db-postgres/src/find/buildFindManyArgs.ts @@ -1,19 +1,22 @@ -import { ArrayField, Block } from 'payload/types'; -import toSnakeCase from 'to-snake-case'; -import { SanitizedCollectionConfig } from 'payload/types'; -import { SanitizedConfig } from 'payload/config'; -import { DBQueryConfig } from 'drizzle-orm'; -import { traverseFields } from './traverseFields'; -import { buildWithFromDepth } from './buildWithFromDepth'; -import { createLocaleWhereQuery } from './createLocaleWhereQuery'; -import { PostgresAdapter } from '../types'; +import type { DBQueryConfig } from 'drizzle-orm' +import type { SanitizedConfig } from 'payload/config' +import type { ArrayField, Block } from 'payload/types' +import type { SanitizedCollectionConfig } from 'payload/types' + +import toSnakeCase from 'to-snake-case' + +import type { PostgresAdapter } from '../types' + +import { buildWithFromDepth } from './buildWithFromDepth' +import { createLocaleWhereQuery } from './createLocaleWhereQuery' +import { traverseFields } from './traverseFields' type BuildFindQueryArgs = { adapter: PostgresAdapter - config: SanitizedConfig collection: SanitizedCollectionConfig + config: SanitizedConfig depth: number - fallbackLocale?: string | false + fallbackLocale?: false | string locale?: string } @@ -23,33 +26,33 @@ export type Result = DBQueryConfig<'many', true, any, any> // a collection field structure export const buildFindManyArgs = ({ adapter, - config, collection, + config, depth, fallbackLocale, locale, }: BuildFindQueryArgs): Record => { const result: Result = { with: {}, - }; + } const _locales: Result = { - where: createLocaleWhereQuery({ fallbackLocale, locale }), columns: { - id: false, _parentID: false, + id: false, }, - }; + where: createLocaleWhereQuery({ fallbackLocale, locale }), + } - const tableName = toSnakeCase(collection.slug); + const tableName = toSnakeCase(collection.slug) if (adapter.tables[`${tableName}_relationships`]) { result.with._relationships = { - orderBy: ({ order }, { asc }) => [asc(order)], columns: { id: false, parent: false, }, + orderBy: ({ order }, { asc }) => [asc(order)], with: buildWithFromDepth({ adapter, config, @@ -57,30 +60,30 @@ export const buildFindManyArgs = ({ fallbackLocale, locale, }), - }; + } } if (adapter.tables[`${tableName}_locales`]) { - result.with._locales = _locales; + result.with._locales = _locales } - const locatedBlocks: Block[] = []; - const locatedArrays: { [path: string]: ArrayField } = {}; + const locatedBlocks: Block[] = [] + const locatedArrays: { [path: string]: ArrayField } = {} traverseFields({ + _locales, adapter, config, currentArgs: result, currentTableName: tableName, depth, fields: collection.fields, - _locales, locatedArrays, locatedBlocks, path: '', topLevelArgs: result, topLevelTableName: tableName, - }); + }) - return result; -}; + return result +} diff --git a/packages/db-postgres/src/find/buildWithFromDepth.ts b/packages/db-postgres/src/find/buildWithFromDepth.ts index 3ce96d24e..f4c79d140 100644 --- a/packages/db-postgres/src/find/buildWithFromDepth.ts +++ b/packages/db-postgres/src/find/buildWithFromDepth.ts @@ -1,13 +1,15 @@ /* eslint-disable no-param-reassign */ -import { SanitizedConfig } from 'payload/config'; -import { buildFindManyArgs } from './buildFindManyArgs'; -import { PostgresAdapter } from '../types'; +import type { SanitizedConfig } from 'payload/config' + +import type { PostgresAdapter } from '../types' + +import { buildFindManyArgs } from './buildFindManyArgs' type BuildWithFromDepthArgs = { adapter: PostgresAdapter config: SanitizedConfig depth: number - fallbackLocale?: string | false + fallbackLocale?: false | string locale?: string } @@ -19,23 +21,23 @@ export const buildWithFromDepth = ({ locale, }: BuildWithFromDepthArgs): Record | undefined => { const result = config.collections.reduce((slugs, coll) => { - const { slug } = coll; + const { slug } = coll if (depth >= 1) { const args = buildFindManyArgs({ adapter, - config, collection: coll, + config, depth: depth - 1, fallbackLocale, locale, - }); + }) - slugs[`${slug}ID`] = args; + slugs[`${slug}ID`] = args } - return slugs; - }, {}); + return slugs + }, {}) - return result; -}; + return result +} diff --git a/packages/db-postgres/src/find/createLocaleWhereQuery.ts b/packages/db-postgres/src/find/createLocaleWhereQuery.ts index ddb943e81..8eca07d2e 100644 --- a/packages/db-postgres/src/find/createLocaleWhereQuery.ts +++ b/packages/db-postgres/src/find/createLocaleWhereQuery.ts @@ -1,12 +1,13 @@ type CreateLocaleWhereQueryArgs = { - fallbackLocale?: string | false + fallbackLocale?: false | string locale?: string } export const createLocaleWhereQuery = ({ fallbackLocale, locale }: CreateLocaleWhereQueryArgs) => { - if (!locale || locale === 'all') return undefined; + if (!locale || locale === 'all') return undefined - if (fallbackLocale) return ({ _locale }, { or, eq }) => or(eq(_locale, locale), eq(_locale, fallbackLocale)); + if (fallbackLocale) + return ({ _locale }, { eq, or }) => or(eq(_locale, locale), eq(_locale, fallbackLocale)) - return ({ _locale }, { eq }) => eq(_locale, locale); -}; + return ({ _locale }, { eq }) => eq(_locale, locale) +} diff --git a/packages/db-postgres/src/find/traverseFields.ts b/packages/db-postgres/src/find/traverseFields.ts index ddc3302b3..ecd0c55ee 100644 --- a/packages/db-postgres/src/find/traverseFields.ts +++ b/packages/db-postgres/src/find/traverseFields.ts @@ -1,34 +1,36 @@ /* eslint-disable no-param-reassign */ -import { SanitizedConfig } from 'payload/config'; -import toSnakeCase from 'to-snake-case'; -import { fieldAffectsData } from 'payload/types'; -import { ArrayField, Block, Field } from 'payload/types'; -import { Result } from './buildFindManyArgs'; -import { PostgresAdapter } from '../types'; +import type { SanitizedConfig } from 'payload/config' +import type { ArrayField, Block, Field } from 'payload/types' + +import { fieldAffectsData } from 'payload/types' +import toSnakeCase from 'to-snake-case' + +import type { PostgresAdapter } from '../types' +import type { Result } from './buildFindManyArgs' type TraverseFieldArgs = { - adapter: PostgresAdapter - config: SanitizedConfig, - currentArgs: Record, - currentTableName: string - depth?: number, - fields: Field[] _locales: Record - locatedArrays: { [path: string]: ArrayField }, - locatedBlocks: Block[], - path: string, - topLevelArgs: Record, + adapter: PostgresAdapter + config: SanitizedConfig + currentArgs: Record + currentTableName: string + depth?: number + fields: Field[] + locatedArrays: { [path: string]: ArrayField } + locatedBlocks: Block[] + path: string + topLevelArgs: Record topLevelTableName: string } export const traverseFields = ({ + _locales, adapter, config, currentArgs, currentTableName, depth, fields, - _locales, locatedArrays, locatedBlocks, path, @@ -40,40 +42,40 @@ export const traverseFields = ({ switch (field.type) { case 'array': { const withArray: Result = { - orderBy: ({ _order }, { asc }) => [asc(_order)], columns: { - _parentID: false, _order: false, + _parentID: false, }, + orderBy: ({ _order }, { asc }) => [asc(_order)], with: {}, - }; + } - const arrayTableName = `${currentTableName}_${toSnakeCase(field.name)}`; + const arrayTableName = `${currentTableName}_${toSnakeCase(field.name)}` - if (adapter.tables[`${arrayTableName}_locales`]) withArray.with._locales = _locales; - currentArgs.with[`${path}${field.name}`] = withArray; + if (adapter.tables[`${arrayTableName}_locales`]) withArray.with._locales = _locales + currentArgs.with[`${path}${field.name}`] = withArray traverseFields({ + _locales, adapter, config, currentArgs: withArray, currentTableName: arrayTableName, depth, fields: field.fields, - _locales, locatedArrays, locatedBlocks, path: '', topLevelArgs, topLevelTableName, - }); + }) - break; + break } case 'blocks': field.blocks.forEach((block) => { - const blockKey = `_blocks_${block.slug}`; + const blockKey = `_blocks_${block.slug}` if (!topLevelArgs[blockKey]) { const withBlock: Result = { @@ -82,54 +84,55 @@ export const traverseFields = ({ }, orderBy: ({ _order }, { asc }) => [asc(_order)], with: {}, - }; + } - if (adapter.tables[`${topLevelArgs}_${toSnakeCase(block.slug)}_locales`]) withBlock.with._locales = _locales; - topLevelArgs.with[blockKey] = withBlock; + if (adapter.tables[`${topLevelArgs}_${toSnakeCase(block.slug)}_locales`]) + withBlock.with._locales = _locales + topLevelArgs.with[blockKey] = withBlock traverseFields({ + _locales, adapter, config, currentArgs: withBlock, currentTableName, depth, fields: block.fields, - _locales, locatedArrays, locatedBlocks, path, topLevelArgs, topLevelTableName, - }); + }) } - }); + }) - break; + break case 'group': traverseFields({ + _locales, adapter, config, currentArgs, currentTableName, depth, fields: field.fields, - _locales, locatedArrays, locatedBlocks, path: `${path}${field.name}_`, topLevelArgs, topLevelTableName, - }); + }) - break; + break default: { - break; + break } } } - }); + }) - return topLevelArgs; -}; + return topLevelArgs +} diff --git a/packages/db-postgres/src/findOne.ts b/packages/db-postgres/src/findOne.ts index 2eb34bc62..bc98c5413 100644 --- a/packages/db-postgres/src/findOne.ts +++ b/packages/db-postgres/src/findOne.ts @@ -1,47 +1,49 @@ -import toSnakeCase from 'to-snake-case'; -import type { FindOne } from 'payload/database'; -import type { PayloadRequest } from 'payload/types'; -import type { SanitizedCollectionConfig } from 'payload/types'; -import buildQuery from './queries/buildQuery'; -import { buildFindManyArgs } from './find/buildFindManyArgs'; -import { transform } from './transform/read'; +import type { FindOne } from 'payload/database' +import type { SanitizedCollectionConfig } from 'payload/types' +import type { PayloadRequest } from 'payload/types' + +import toSnakeCase from 'to-snake-case' + +import { buildFindManyArgs } from './find/buildFindManyArgs' +import buildQuery from './queries/buildQuery' +import { transform } from './transform/read' export const findOne: FindOne = async function findOne({ collection, - where, locale, req = {} as PayloadRequest, + where, }) { - const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config; - const tableName = toSnakeCase(collection); + const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config + const tableName = toSnakeCase(collection) const query = await buildQuery({ adapter: this, collectionSlug: collection, locale, where, - }); + }) const findManyArgs = buildFindManyArgs({ adapter: this, - config: this.payload.config, collection: collectionConfig, + config: this.payload.config, depth: 0, fallbackLocale: req.fallbackLocale, locale: req.locale, - }); + }) - findManyArgs.where = query; + findManyArgs.where = query - const doc = await this.db.query[tableName].findFirst(findManyArgs); + const doc = await this.db.query[tableName].findFirst(findManyArgs) const result = transform({ config: this.payload.config, - fallbackLocale: req.fallbackLocale, - locale: req.locale, data: doc, + fallbackLocale: req.fallbackLocale, fields: collectionConfig.fields, - }); + locale: req.locale, + }) - return result; -}; + return result +} diff --git a/packages/db-postgres/src/index.ts b/packages/db-postgres/src/index.ts index da9689819..176790d9a 100644 --- a/packages/db-postgres/src/index.ts +++ b/packages/db-postgres/src/index.ts @@ -1,26 +1,29 @@ -import type { Payload } from 'payload'; -import { createDatabaseAdapter } from 'payload/database'; -import { connect } from './connect'; -import { init } from './init'; -import { createMigration } from './createMigration'; -import { webpack } from './webpack'; -import { Args, PostgresAdapter, PostgresAdapterResult } from './types'; +import type { Payload } from 'payload' + +import { createDatabaseAdapter } from 'payload/database' + +import type { Args, PostgresAdapter, PostgresAdapterResult } from './types' + +import { connect } from './connect' +import { createMigration } from './createMigration' +import { init } from './init' +import { webpack } from './webpack' // import { createGlobal } from './createGlobal'; // import { createVersion } from './createVersion'; // import { beginTransaction } from './transactions/beginTransaction'; // import { rollbackTransaction } from './transactions/rollbackTransaction'; // import { commitTransaction } from './transactions/commitTransaction'; // import { queryDrafts } from './queryDrafts'; -import { find } from './find'; +import { find } from './find' // import { findGlobalVersions } from './findGlobalVersions'; // import { findVersions } from './findVersions'; -import { create } from './create'; +import { create } from './create' // import { deleteOne } from './deleteOne'; // import { deleteVersions } from './deleteVersions'; // import { findGlobal } from './findGlobal'; -import { findOne } from './findOne'; +import { findOne } from './findOne' // import { updateGlobal } from './updateGlobal'; -import { updateOne } from './update'; +import { updateOne } from './update' // import { updateVersion } from './updateVersion'; // import { deleteMany } from './deleteMany'; // import { destroy } from './destroy'; @@ -31,24 +34,24 @@ export function postgresAdapter(args: Args): PostgresAdapterResult { // @ts-expect-error return createDatabaseAdapter({ ...args, - enums: {}, - relations: {}, - tables: {}, - payload, connect, + create, + createMigration, db: undefined, + enums: {}, + find, + // queryDrafts, + findOne, // destroy, init, - webpack, - createMigration, + payload, // beginTransaction, // rollbackTransaction, // commitTransaction, - // queryDrafts, - findOne, - find, - create, + relations: {}, + tables: {}, updateOne, + webpack, // deleteOne, // deleteMany, // findGlobal, @@ -59,8 +62,8 @@ export function postgresAdapter(args: Args): PostgresAdapterResult { // createVersion, // updateVersion, // deleteVersions, - }); + }) } - return adapter; + return adapter } diff --git a/packages/db-postgres/src/init.ts b/packages/db-postgres/src/init.ts index 0301f46da..90f9255d9 100644 --- a/packages/db-postgres/src/init.ts +++ b/packages/db-postgres/src/init.ts @@ -1,17 +1,20 @@ /* eslint-disable no-param-reassign */ -import { pgEnum } from 'drizzle-orm/pg-core'; +import type { Init } from 'payload/database' // import { SanitizedCollectionConfig } from 'payload/dist/collections/config/types'; -import { SanitizedCollectionConfig } from 'payload/types'; -import type { Init } from 'payload/database'; -import { buildTable } from './schema/build'; -import type { PostgresAdapter } from './types'; +import type { SanitizedCollectionConfig } from 'payload/types' + +import { pgEnum } from 'drizzle-orm/pg-core' + +import type { PostgresAdapter } from './types' + +import { buildTable } from './schema/build' export const init: Init = async function init(this: PostgresAdapter) { if (this.payload.config.localization) { this.enums._locales = pgEnum( '_locales', this.payload.config.localization.locales as [string, ...string[]], - ); + ) } this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => { @@ -21,10 +24,10 @@ export const init: Init = async function init(this: PostgresAdapter) { fields: collection.fields, tableName: collection.slug, timestamps: collection.timestamps, - }); - }); + }) + }) this.payload.config.globals.forEach((global) => { // create global model - }); -}; + }) +} diff --git a/packages/db-postgres/src/insertArrays.ts b/packages/db-postgres/src/insertArrays.ts index d82bb63b1..0f52bf2ef 100644 --- a/packages/db-postgres/src/insertArrays.ts +++ b/packages/db-postgres/src/insertArrays.ts @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ -import { PostgresAdapter } from './types'; -import { ArrayRowToInsert } from './transform/write/types'; +import type { ArrayRowToInsert } from './transform/write/types' +import type { PostgresAdapter } from './types' type Args = { adapter: PostgresAdapter @@ -12,13 +12,13 @@ type Args = { type RowsByTable = { [tableName: string]: { - rows: Record[] - locales: Record[] - columnName: string - rowIndexMap: [number, number][] arrays: { [tableName: string]: ArrayRowToInsert[] }[] + columnName: string + locales: Record[] + rowIndexMap: [number, number][] + rows: Record[] } } // We want to insert ALL array rows per table with a single insertion @@ -26,91 +26,92 @@ type RowsByTable = { // To do this, we take in an array of arrays by table name and parent rows // Parent rows and the array of arrays need to be the same length // so we can "hoist" the created array rows back into the parent rows -export const insertArrays = async ({ - adapter, - arrays, - parentRows, -}: Args): Promise => { +export const insertArrays = async ({ adapter, arrays, parentRows }: Args): Promise => { // Maintain a map of flattened rows by table - const rowsByTable: RowsByTable = {}; + const rowsByTable: RowsByTable = {} arrays.forEach((arraysByTable, parentRowIndex) => { Object.entries(arraysByTable).forEach(([tableName, arrayRows]) => { // If the table doesn't exist in map, initialize it if (!rowsByTable[tableName]) { rowsByTable[tableName] = { - rows: [], - locales: [], - columnName: arrayRows[0]?.columnName, - rowIndexMap: [], arrays: [], - }; + columnName: arrayRows[0]?.columnName, + locales: [], + rowIndexMap: [], + rows: [], + } } - const parentID = parentRows[parentRowIndex].id; + const parentID = parentRows[parentRowIndex].id // We store row indexes to "slice out" the array rows // that belong to each parent row rowsByTable[tableName].rowIndexMap.push([ - rowsByTable[tableName].rows.length, rowsByTable[tableName].rows.length + arrayRows.length, - ]); + rowsByTable[tableName].rows.length, + rowsByTable[tableName].rows.length + arrayRows.length, + ]) // Add any sub arrays that need to be created // We will call this recursively below arrayRows.forEach((arrayRow) => { if (Object.keys(arrayRow.arrays).length > 0) { - rowsByTable[tableName].arrays.push(arrayRow.arrays); + rowsByTable[tableName].arrays.push(arrayRow.arrays) } // Set up parent IDs for both row and locale row - arrayRow.row._parentID = parentID; - rowsByTable[tableName].rows.push(arrayRow.row); - arrayRow.locale._parentID = arrayRow.row.id; - rowsByTable[tableName].locales.push(arrayRow.locale); - }); - }); - }); + arrayRow.row._parentID = parentID + rowsByTable[tableName].rows.push(arrayRow.row) + arrayRow.locale._parentID = arrayRow.row.id + rowsByTable[tableName].locales.push(arrayRow.locale) + }) + }) + }) // Insert all corresponding arrays in parallel // (one insert per array table) - await Promise.all(Object.entries(rowsByTable).map(async ( - [tableName, row], - ) => { - const insertedRows = await adapter.db.insert(adapter.tables[tableName]) - .values(row.rows).returning(); + await Promise.all( + Object.entries(rowsByTable).map(async ([tableName, row]) => { + const insertedRows = await adapter.db + .insert(adapter.tables[tableName]) + .values(row.rows) + .returning() - rowsByTable[tableName].rows = insertedRows.map((arrayRow) => { - delete arrayRow._parentID; - delete arrayRow._order; - return arrayRow; - }); + rowsByTable[tableName].rows = insertedRows.map((arrayRow) => { + delete arrayRow._parentID + delete arrayRow._order + return arrayRow + }) - // Insert locale rows - if (adapter.tables[`${tableName}_locales`]) { - const insertedLocaleRows = await adapter.db.insert(adapter.tables[`${tableName}_locales`]) - .values(row.locales).returning(); + // Insert locale rows + if (adapter.tables[`${tableName}_locales`]) { + const insertedLocaleRows = await adapter.db + .insert(adapter.tables[`${tableName}_locales`]) + .values(row.locales) + .returning() - insertedLocaleRows.forEach((localeRow, i) => { - delete localeRow._parentID; - rowsByTable[tableName].rows[i]._locales = [localeRow]; - }); - } + insertedLocaleRows.forEach((localeRow, i) => { + delete localeRow._parentID + rowsByTable[tableName].rows[i]._locales = [localeRow] + }) + } - // If there are sub arrays, call this function recursively - if (row.arrays.length > 0) { - await insertArrays({ - adapter, - arrays: row.arrays, - parentRows: row.rows, - }); - } - })); + // If there are sub arrays, call this function recursively + if (row.arrays.length > 0) { + await insertArrays({ + adapter, + arrays: row.arrays, + parentRows: row.rows, + }) + } + }), + ) // Finally, hoist up the newly inserted arrays to their parent row // by slicing out the appropriate range from rowIndexMap - Object.values(rowsByTable).forEach(({ rows, columnName, rowIndexMap }) => { + Object.values(rowsByTable).forEach(({ columnName, rowIndexMap, rows }) => { rowIndexMap.forEach(([start, finish], i) => { - parentRows[i][columnName] = rows.slice(start, finish); - }); - }); -}; + parentRows[i][columnName] = rows.slice(start, finish) + }) + }) +} diff --git a/packages/db-postgres/src/mock.js b/packages/db-postgres/src/mock.js index 2d27fcce3..10c24eb11 100644 --- a/packages/db-postgres/src/mock.js +++ b/packages/db-postgres/src/mock.js @@ -1 +1 @@ -exports.postgresAdapter = () => ({}); +exports.postgresAdapter = () => ({}) diff --git a/packages/db-postgres/src/queries/buildAndOrConditions.ts b/packages/db-postgres/src/queries/buildAndOrConditions.ts index 3f43177b6..7fa7c295e 100644 --- a/packages/db-postgres/src/queries/buildAndOrConditions.ts +++ b/packages/db-postgres/src/queries/buildAndOrConditions.ts @@ -1,25 +1,27 @@ -import { Where } from 'payload/types'; -import { Field } from 'payload/types'; -import { SQL } from 'drizzle-orm'; -import { parseParams } from './parseParams'; -import { PostgresAdapter } from '../types'; +import type { SQL } from 'drizzle-orm' +import type { Field } from 'payload/types' +import type { Where } from 'payload/types' + +import type { PostgresAdapter } from '../types' + +import { parseParams } from './parseParams' export async function buildAndOrConditions({ - where, - collectionSlug, - globalSlug, adapter, - locale, + collectionSlug, fields, + globalSlug, + locale, + where, }: { - where: Where[], - collectionSlug?: string, - globalSlug?: string, adapter: PostgresAdapter - locale?: string, - fields: Field[], + collectionSlug?: string + fields: Field[] + globalSlug?: string + locale?: string + where: Where[] }): Promise { - const completedConditions = []; + const completedConditions = [] // Loop over all AND / OR operations and add them to the AND / OR query param // Operations should come through as an array // eslint-disable-next-line no-restricted-syntax @@ -28,17 +30,17 @@ export async function buildAndOrConditions({ if (typeof condition === 'object') { // eslint-disable-next-line no-await-in-loop const result = await parseParams({ - where: condition, - collectionSlug, - globalSlug, adapter, - locale, + collectionSlug, fields, - }); + globalSlug, + locale, + where: condition, + }) if (Object.keys(result).length > 0) { - completedConditions.push(result); + completedConditions.push(result) } } } - return completedConditions; + return completedConditions } diff --git a/packages/db-postgres/src/queries/buildQuery.ts b/packages/db-postgres/src/queries/buildQuery.ts index bebe3ed53..64405b34c 100644 --- a/packages/db-postgres/src/queries/buildQuery.ts +++ b/packages/db-postgres/src/queries/buildQuery.ts @@ -1,53 +1,56 @@ -import { Where } from 'payload/types'; -import { Field } from 'payload/types'; -import { QueryError } from 'payload/errors'; -import { SQL } from 'drizzle-orm'; -import { parseParams } from './parseParams'; -import { PostgresAdapter } from '../types'; +import type { SQL } from 'drizzle-orm' +import type { Where } from 'payload/types' +import type { Field } from 'payload/types' + +import { QueryError } from 'payload/errors' + +import type { PostgresAdapter } from '../types' + +import { parseParams } from './parseParams' type BuildQueryArgs = { adapter: PostgresAdapter - where: Where - locale?: string collectionSlug?: string globalSlug?: string + locale?: string versionsFields?: Field[] + where: Where } const buildQuery = async function buildQuery({ adapter, - where, - locale, collectionSlug, globalSlug, + locale, versionsFields, + where, }: BuildQueryArgs): Promise { - let fields = versionsFields; + let fields = versionsFields if (!fields) { if (globalSlug) { - const globalConfig = adapter.payload.globals.config.find(({ slug }) => slug === globalSlug); - fields = globalConfig.fields; + const globalConfig = adapter.payload.globals.config.find(({ slug }) => slug === globalSlug) + fields = globalConfig.fields } if (collectionSlug) { - const collectionConfig = adapter.payload.collections[collectionSlug].config; - fields = collectionConfig.fields; + const collectionConfig = adapter.payload.collections[collectionSlug].config + fields = collectionConfig.fields } } - const errors = []; + const errors = [] const result = await parseParams({ + adapter, collectionSlug, fields, globalSlug, - adapter, locale, where, - }); + }) if (errors.length > 0) { - throw new QueryError(errors); + throw new QueryError(errors) } - return result; -}; + return result +} -export default buildQuery; +export default buildQuery diff --git a/packages/db-postgres/src/queries/buildSearchParams.ts b/packages/db-postgres/src/queries/buildSearchParams.ts index f15f50b6f..d7dbe8a5f 100644 --- a/packages/db-postgres/src/queries/buildSearchParams.ts +++ b/packages/db-postgres/src/queries/buildSearchParams.ts @@ -1,93 +1,95 @@ -import toSnakeCase from 'to-snake-case'; -import { inArray } from 'drizzle-orm'; -import { SQL } from 'drizzle-orm'; -import { getLocalizedPaths } from 'payload/database'; -import { Field } from 'payload/types'; -import { fieldAffectsData } from 'payload/types'; -import { PathToQuery } from 'payload/database'; -import { validOperators } from 'payload/types'; -import { Operator } from 'payload/types'; -import { operatorMap } from './operatorMap'; -import { PostgresAdapter } from '../types'; +import type { SQL } from 'drizzle-orm' +import type { PathToQuery } from 'payload/database' +import type { Field } from 'payload/types' +import type { Operator } from 'payload/types' + +import { inArray } from 'drizzle-orm' +import { getLocalizedPaths } from 'payload/database' +import { fieldAffectsData } from 'payload/types' +import { validOperators } from 'payload/types' +import toSnakeCase from 'to-snake-case' + +import type { PostgresAdapter } from '../types' + +import { operatorMap } from './operatorMap' type SearchParam = { - path?: string, + path?: string // TODO: possible better type // value: SQL - value: SQL, + value: SQL } /** * Convert the Payload key / value / operator into a Drizzle query */ export async function buildSearchParam({ - fields, - incomingPath, - val, - operator, - collectionSlug, - globalSlug, adapter, + collectionSlug, + fields, + globalSlug, + incomingPath, locale, + operator, + val, }: { - fields: Field[], - incomingPath: string, - val: unknown, - operator: string - collectionSlug?: string, - globalSlug?: string, adapter: PostgresAdapter + collectionSlug?: string + fields: Field[] + globalSlug?: string + incomingPath: string locale?: string + operator: string + val: unknown }): Promise { // Replace GraphQL nested field double underscore formatting - const sanitizedPath = incomingPath.replace(/__/gi, '.'); + const sanitizedPath = incomingPath.replace(/__/g, '.') - let paths: PathToQuery[] = []; + let paths: PathToQuery[] = [] - let hasCustomID = false; + let hasCustomID = false if (sanitizedPath === 'id') { - const customIDfield = adapter.payload.collections[collectionSlug]?.config.fields.find((field) => fieldAffectsData(field) && field.name === 'id'); + const customIDfield = adapter.payload.collections[collectionSlug]?.config.fields.find( + (field) => fieldAffectsData(field) && field.name === 'id', + ) - let idFieldType: 'text' | 'number' = 'text'; + let idFieldType: 'number' | 'text' = 'text' if (customIDfield) { if (customIDfield?.type === 'text' || customIDfield?.type === 'number') { - idFieldType = customIDfield.type; + idFieldType = customIDfield.type } - hasCustomID = true; + hasCustomID = true } paths.push({ - path: 'id', + collectionSlug, + complete: true, field: { name: 'id', type: idFieldType, } as Field, - complete: true, - collectionSlug, - }); + path: 'id', + }) } else { // TODO: handle differently paths = await getLocalizedPaths({ - payload: adapter.payload, - locale, collectionSlug, - globalSlug, fields, + globalSlug, incomingPath: sanitizedPath, - }); + locale, + payload: adapter.payload, + }) } - const [{ - path, - field, - }] = paths; + const [{ field, path }] = paths if (path) { // TODO: determine if sanitizeQueryValue is needed or not - const formattedValue = val; + const formattedValue = val // const formattedValue = sanitizeQueryValue({ // field, // path, @@ -101,91 +103,90 @@ export async function buildSearchParam({ if (paths.length > 1) { // Remove top collection and reverse array // to work backwards from top - const pathsToQuery = paths.slice(1) - .reverse(); + const pathsToQuery = paths.slice(1).reverse() const initialRelationshipQuery = { value: {}, - } as SearchParam; + } as SearchParam - const relationshipQuery = await pathsToQuery.reduce(async (priorQuery, { - path: subPath, - collectionSlug: slug, - }, i) => { - const priorQueryResult = await priorQuery; - const tableName = toSnakeCase(slug); + const relationshipQuery = await pathsToQuery.reduce( + async (priorQuery, { collectionSlug: slug, path: subPath }, i) => { + const priorQueryResult = await priorQuery + const tableName = toSnakeCase(slug) - // On the "deepest" collection, - // Search on the value passed through the query - if (i === 0) { - // TODO: switch on field type of subPath to query the correct table for: - // arrays - // blocks - // localized - const table = adapter.tables[tableName]; - const result = await adapter.db - .select() - .from(table) - .where(operatorMap[operator](table[subPath], val)); + // On the "deepest" collection, + // Search on the value passed through the query + if (i === 0) { + // TODO: switch on field type of subPath to query the correct table for: + // arrays + // blocks + // localized + const table = adapter.tables[tableName] + const result = await adapter.db + .select() + .from(table) + .where(operatorMap[operator](table[subPath], val)) - const relatedIDs: (string | number)[] = []; + const relatedIDs: (number | string)[] = [] - result.forEach((row: { id: string | number, [key: string]: unknown }) => { - relatedIDs.push(row.id); - }); + result.forEach((row: { [key: string]: unknown; id: number | string }) => { + relatedIDs.push(row.id) + }) + + if (pathsToQuery.length === 1) { + return { + path, + value: inArray(table.id, relatedIDs), + } + } + + const nextSubPath = pathsToQuery[i + 1].path + const nextSubTableName = toSnakeCase(path) - if (pathsToQuery.length === 1) { return { - path, - value: inArray(table.id, relatedIDs), - }; + value: { [nextSubPath]: inArray(adapter.tables[nextSubTableName].id, relatedIDs) }, + } } - const nextSubPath = pathsToQuery[i + 1].path; - const nextSubTableName = toSnakeCase(path); + const subQuery = priorQueryResult.value + const subQueryTable = toSnakeCase(priorQueryResult.path) + + // TODO: handle querying from adjacent tables (array, blocks, localization) + const result = await adapter.db + // TODO: only select id? + .select() + .from(subQueryTable) + .where(subQuery) + + const relatedIDs = result.map(({ id }) => id) + // TODO: not sure if this is tableName or subQueryTable + const table = adapter.tables[tableName] + + // If it is the last recursion + // then pass through the search param + if (i + 1 === pathsToQuery.length) { + return { + path, + value: inArray(subQueryTable.id, relatedIDs), + } + } return { - value: { [nextSubPath]: inArray(adapter.tables[nextSubTableName].id, relatedIDs) }, - }; - } + value: inArray(table.id, relatedIDs), + } + }, + Promise.resolve(initialRelationshipQuery), + ) - const subQuery = priorQueryResult.value; - const subQueryTable = toSnakeCase(priorQueryResult.path); - - // TODO: handle querying from adjacent tables (array, blocks, localization) - const result = await adapter.db - // TODO: only select id? - .select() - .from(subQueryTable) - .where(subQuery as SQL); - - const relatedIDs = result.map(({ id }) => id); - // TODO: not sure if this is tableName or subQueryTable - const table = adapter.tables[tableName]; - - // If it is the last recursion - // then pass through the search param - if (i + 1 === pathsToQuery.length) { - return { - path, - value: inArray(subQueryTable.id, relatedIDs), - }; - } - - return { - value: inArray(table.id, relatedIDs), - }; - }, Promise.resolve(initialRelationshipQuery)); - - return relationshipQuery; + return relationshipQuery } if (operator && validOperators.includes(operator as Operator)) { - const operatorKey = operatorMap[operator]; + const operatorKey = operatorMap[operator] if (field.type === 'relationship' || field.type === 'upload') { - console.log('not implemented'); - let hasNumberIDRelation; + console.log('not implemented') + let hasNumberIDRelation // const result = { // value: { @@ -221,7 +222,7 @@ export async function buildSearchParam({ // TODO: rewrite like or use drizzle's like operator? if (operator === 'like' && typeof formattedValue === 'string') { - const words = formattedValue.split(' '); + const words = formattedValue.split(' ') // const result = { // value: { @@ -246,12 +247,12 @@ export async function buildSearchParam({ // }; // } - const table = adapter.tables[toSnakeCase(collectionSlug)]; + const table = adapter.tables[toSnakeCase(collectionSlug)] return { path, value: operatorKey(table[path], formattedValue), - }; + } } } - return undefined; + return undefined } diff --git a/packages/db-postgres/src/queries/operatorMap.ts b/packages/db-postgres/src/queries/operatorMap.ts index 9ee000d1c..90cd9d4c5 100644 --- a/packages/db-postgres/src/queries/operatorMap.ts +++ b/packages/db-postgres/src/queries/operatorMap.ts @@ -1,20 +1,20 @@ -import { and, eq, gt, gte, inArray, isNotNull, lt, lte, ne, notInArray, or } from 'drizzle-orm'; +import { and, eq, gt, gte, inArray, isNotNull, lt, lte, ne, notInArray, or } from 'drizzle-orm' export const operatorMap = { - greater_than_equal: gte, - less_than_equal: lte, - less_than: lt, + // near: near, + and, + equals: eq, + // TODO: isNotNull isn't right as it depends on if the query value is true or false + exists: isNotNull, greater_than: gt, + greater_than_equal: gte, + // TODO: in: inArray, + less_than: lt, + less_than_equal: lte, + not_equals: ne, // TODO: // all: all, not_in: notInArray, - not_equals: ne, - // TODO: isNotNull isn't right as it depends on if the query value is true or false - exists: isNotNull, - equals: eq, - // TODO: - // near: near, - and, or, -}; +} diff --git a/packages/db-postgres/src/queries/parseParams.ts b/packages/db-postgres/src/queries/parseParams.ts index d87fdeabd..44ac305c1 100644 --- a/packages/db-postgres/src/queries/parseParams.ts +++ b/packages/db-postgres/src/queries/parseParams.ts @@ -1,78 +1,82 @@ /* eslint-disable no-restricted-syntax */ +import type { SQL } from 'drizzle-orm' /* eslint-disable no-await-in-loop */ -import { Operator, Where } from 'payload/types'; -import { Field } from 'payload/types'; -import { validOperators } from 'payload/types'; -import { and, SQL } from 'drizzle-orm'; -import { buildSearchParam } from './buildSearchParams'; -import { buildAndOrConditions } from './buildAndOrConditions'; -import { PostgresAdapter } from '../types'; +import type { Operator, Where } from 'payload/types' +import type { Field } from 'payload/types' + +import { and } from 'drizzle-orm' +import { validOperators } from 'payload/types' + +import type { PostgresAdapter } from '../types' + +import { buildAndOrConditions } from './buildAndOrConditions' +import { buildSearchParam } from './buildSearchParams' export async function parseParams({ - where, - collectionSlug, - globalSlug, adapter, - locale, + collectionSlug, fields, + globalSlug, + locale, + where, }: { - where: Where, - collectionSlug?: string, - globalSlug?: string, adapter: PostgresAdapter - locale: string, - fields: Field[], + collectionSlug?: string + fields: Field[] + globalSlug?: string + locale: string + where: Where }): Promise { - let result: SQL; + let result: SQL if (typeof where === 'object') { // We need to determine if the whereKey is an AND, OR, or a schema path for (const relationOrPath of Object.keys(where)) { - const condition = where[relationOrPath]; - let conditionOperator: 'and' | 'or'; + const condition = where[relationOrPath] + let conditionOperator: 'and' | 'or' if (relationOrPath.toLowerCase() === 'and') { - conditionOperator = 'and'; + conditionOperator = 'and' } else if (relationOrPath.toLowerCase() === 'or') { - conditionOperator = 'or'; + conditionOperator = 'or' } if (Array.isArray(condition)) { const builtConditions = await buildAndOrConditions({ + adapter, collectionSlug, fields, globalSlug, - adapter, locale, where: condition, - }); + }) - if (builtConditions.length > 0) result = and(result, ...builtConditions); + if (builtConditions.length > 0) result = and(result, ...builtConditions) } else { // It's a path - and there can be multiple comparisons on a single path. // For example - title like 'test' and title not equal to 'tester' // So we need to loop on keys again here to handle each operator independently - const pathOperators = where[relationOrPath]; + const pathOperators = where[relationOrPath] if (typeof pathOperators === 'object') { for (const operator of Object.keys(pathOperators)) { if (validOperators.includes(operator as Operator)) { const searchParam = await buildSearchParam({ - collectionSlug, - globalSlug, adapter, - locale, + collectionSlug, fields, + globalSlug, incomingPath: relationOrPath, - val: pathOperators[operator], + locale, operator, - }); + val: pathOperators[operator], + }) if (searchParam?.value && searchParam?.path) { - result = and(result, searchParam.value); + result = and(result, searchParam.value) // result = { // ...result, // [searchParam.path]: searchParam.value, // }; } else if (typeof searchParam?.value === 'object') { - result = and(result, searchParam.value); + result = and(result, searchParam.value) // result = deepmerge(result, searchParam.value, { arrayMerge: combineMerge }); } } @@ -89,5 +93,5 @@ export async function parseParams({ // ) // ); - return result; + return result } diff --git a/packages/db-postgres/src/reference.ts b/packages/db-postgres/src/reference.ts index 5c4726480..d445c4471 100644 --- a/packages/db-postgres/src/reference.ts +++ b/packages/db-postgres/src/reference.ts @@ -4,40 +4,37 @@ // type PushDiff = (schema: DrizzleSchemaExports) => Promise<{ warnings: string[], apply: () => Promise }> - // drizzle-kit@utils -import { generateDrizzleJson, generateMigration, pushSchema } from 'drizzle-kit/utils'; -import { drizzle } from 'drizzle-orm/node-postgres'; -import { Pool } from 'pg'; +import { generateDrizzleJson, generateMigration, pushSchema } from 'drizzle-kit/utils' +import { drizzle } from 'drizzle-orm/node-postgres' +import { Pool } from 'pg' async function generateUsage() { - const schema = await import('./data/users'); - const schemaAfter = await import('./data/users-after'); + const schema = await import('./data/users') + const schemaAfter = await import('./data/users-after') - const drizzleJsonBefore = generateDrizzleJson(schema); - const drizzleJsonAfter = generateDrizzleJson(schemaAfter); + const drizzleJsonBefore = generateDrizzleJson(schema) + const drizzleJsonAfter = generateDrizzleJson(schemaAfter) - const sqlStatements = await generateMigration(drizzleJsonBefore, drizzleJsonAfter); + const sqlStatements = await generateMigration(drizzleJsonBefore, drizzleJsonAfter) - console.log(sqlStatements); + console.log(sqlStatements) } async function pushUsage() { - const schemaAfter = await import('./data/users-after'); + const schemaAfter = await import('./data/users-after') - const db = drizzle( - new Pool({ connectionString: '' }), - ); + const db = drizzle(new Pool({ connectionString: '' })) - const response = await pushSchema(schemaAfter, db); + const response = await pushSchema(schemaAfter, db) - console.log('\n'); - console.log('hasDataLoss: ', response.hasDataLoss); - console.log('warnings: ', response.warnings); - console.log('statements: ', response.statementsToExecute); + console.log('\n') + console.log('hasDataLoss: ', response.hasDataLoss) + console.log('warnings: ', response.warnings) + console.log('statements: ', response.statementsToExecute) - await response.apply(); + await response.apply() - process.exit(0); + process.exit(0) } diff --git a/packages/db-postgres/src/schema/build.ts b/packages/db-postgres/src/schema/build.ts index c6420a839..f13fca86b 100644 --- a/packages/db-postgres/src/schema/build.ts +++ b/packages/db-postgres/src/schema/build.ts @@ -1,27 +1,30 @@ /* eslint-disable no-param-reassign */ +import type { Relation } from 'drizzle-orm' +import type { AnyPgColumnBuilder, IndexBuilder } from 'drizzle-orm/pg-core' +import type { Field } from 'payload/types' + +import { relations } from 'drizzle-orm' import { - AnyPgColumnBuilder, + index, integer, + numeric, pgTable, serial, - varchar, - index, - numeric, timestamp, - IndexBuilder, unique, -} from 'drizzle-orm/pg-core'; -import { Field } from 'payload/types'; -import toSnakeCase from 'to-snake-case'; -import { Relation, relations } from 'drizzle-orm'; -import { fieldAffectsData } from 'payload/types'; -import { GenericColumns, GenericTable, PostgresAdapter } from '../types'; -import { traverseFields } from './traverseFields'; -import { parentIDColumnMap } from './parentIDColumnMap'; + varchar, +} from 'drizzle-orm/pg-core' +import { fieldAffectsData } from 'payload/types' +import toSnakeCase from 'to-snake-case' + +import type { GenericColumns, GenericTable, PostgresAdapter } from '../types' + +import { parentIDColumnMap } from './parentIDColumnMap' +import { traverseFields } from './traverseFields' type Args = { adapter: PostgresAdapter - baseColumns?: Record, + baseColumns?: Record buildRelationships?: boolean fields: Field[] tableName: string @@ -40,39 +43,39 @@ export const buildTable = ({ tableName, timestamps, }: Args): Result => { - const formattedTableName = toSnakeCase(tableName); - const columns: Record = baseColumns; - const indexes: Record IndexBuilder> = {}; + const formattedTableName = toSnakeCase(tableName) + const columns: Record = baseColumns + const indexes: Record IndexBuilder> = {} - let hasLocalizedField = false; - let hasLocalizedRelationshipField = false; - const localesColumns: Record = {}; - const localesIndexes: Record IndexBuilder> = {}; - let localesTable: GenericTable; + let hasLocalizedField = false + let hasLocalizedRelationshipField = false + const localesColumns: Record = {} + const localesIndexes: Record IndexBuilder> = {} + let localesTable: GenericTable - const relationships: Set = new Set(); - let relationshipsTable: GenericTable; + const relationships: Set = new Set() + let relationshipsTable: GenericTable - const arrayBlockRelations: Map = new Map(); + const arrayBlockRelations: Map = new Map() - const idField = fields.find((field) => fieldAffectsData(field) && field.name === 'id'); - let idColType = 'integer'; + const idField = fields.find((field) => fieldAffectsData(field) && field.name === 'id') + let idColType = 'integer' if (idField) { if (idField.type === 'number') { - idColType = 'numeric'; - columns.id = numeric('id').primaryKey(); + idColType = 'numeric' + columns.id = numeric('id').primaryKey() } if (idField.type === 'text') { - idColType = 'varchar'; - columns.id = varchar('id').primaryKey(); + idColType = 'varchar' + columns.id = varchar('id').primaryKey() } } else { - columns.id = serial('id').primaryKey(); + columns.id = serial('id').primaryKey() } - ({ hasLocalizedField, hasLocalizedRelationshipField } = traverseFields({ + ;({ hasLocalizedField, hasLocalizedRelationshipField } = traverseFields({ adapter, arrayBlockRelations, buildRelationships, @@ -84,128 +87,139 @@ export const buildTable = ({ newTableName: tableName, parentTableName: tableName, relationships, - })); + })) if (timestamps) { - columns.createdAt = timestamp('created_at').defaultNow().notNull(); - columns.updatedAt = timestamp('updated_at').defaultNow().notNull(); + columns.createdAt = timestamp('created_at').defaultNow().notNull() + columns.updatedAt = timestamp('updated_at').defaultNow().notNull() } const table = pgTable(formattedTableName, columns, (cols) => { return Object.entries(indexes).reduce((acc, [colName, func]) => { - acc[colName] = func(cols); - return acc; - }, {}); - }); + acc[colName] = func(cols) + return acc + }, {}) + }) - adapter.tables[formattedTableName] = table; + adapter.tables[formattedTableName] = table if (hasLocalizedField) { - const localeTableName = `${formattedTableName}_locales`; - localesColumns.id = serial('id').primaryKey(); - localesColumns._locale = adapter.enums._locales('_locale').notNull(); - localesColumns._parentID = parentIDColumnMap[idColType]('_parent_id').references(() => table.id).notNull(); + const localeTableName = `${formattedTableName}_locales` + localesColumns.id = serial('id').primaryKey() + localesColumns._locale = adapter.enums._locales('_locale').notNull() + localesColumns._parentID = parentIDColumnMap[idColType]('_parent_id') + .references(() => table.id) + .notNull() localesTable = pgTable(localeTableName, localesColumns, (cols) => { - return Object.entries(localesIndexes).reduce((acc, [colName, func]) => { - acc[colName] = func(cols); - return acc; - }, { - _localeParent: unique().on(cols._locale, cols._parentID), - }); - }); + return Object.entries(localesIndexes).reduce( + (acc, [colName, func]) => { + acc[colName] = func(cols) + return acc + }, + { + _localeParent: unique().on(cols._locale, cols._parentID), + }, + ) + }) - adapter.tables[localeTableName] = localesTable; + adapter.tables[localeTableName] = localesTable const localesTableRelations = relations(localesTable, ({ one }) => ({ _parentID: one(table, { fields: [localesTable._parentID], references: [table.id], }), - })); + })) - adapter.relations[`relations_${localeTableName}`] = localesTableRelations; + adapter.relations[`relations_${localeTableName}`] = localesTableRelations } if (buildRelationships) { if (relationships.size) { const relationshipColumns: Record = { id: serial('id').primaryKey(), - parent: parentIDColumnMap[idColType]('parent_id').references(() => table.id).notNull(), - path: varchar('path').notNull(), order: integer('order'), - }; + parent: parentIDColumnMap[idColType]('parent_id') + .references(() => table.id) + .notNull(), + path: varchar('path').notNull(), + } if (hasLocalizedRelationshipField) { - relationshipColumns.locale = adapter.enums._locale('locale'); + relationshipColumns.locale = adapter.enums._locale('locale') } relationships.forEach((relationTo) => { - const formattedRelationTo = toSnakeCase(relationTo); - let colType = 'integer'; - const relatedCollectionCustomID = adapter.payload.collections[relationTo].config.fields.find((field) => fieldAffectsData(field) && field.name === 'id'); - if (relatedCollectionCustomID?.type === 'number') colType = 'numeric'; - if (relatedCollectionCustomID?.type === 'text') colType = 'varchar'; + const formattedRelationTo = toSnakeCase(relationTo) + let colType = 'integer' + const relatedCollectionCustomID = adapter.payload.collections[ + relationTo + ].config.fields.find((field) => fieldAffectsData(field) && field.name === 'id') + if (relatedCollectionCustomID?.type === 'number') colType = 'numeric' + if (relatedCollectionCustomID?.type === 'text') colType = 'varchar' - relationshipColumns[`${relationTo}ID`] = parentIDColumnMap[colType](`${formattedRelationTo}_id`).references(() => adapter.tables[formattedRelationTo].id); - }); + relationshipColumns[`${relationTo}ID`] = parentIDColumnMap[colType]( + `${formattedRelationTo}_id`, + ).references(() => adapter.tables[formattedRelationTo].id) + }) - const relationshipsTableName = `${formattedTableName}_relationships`; + const relationshipsTableName = `${formattedTableName}_relationships` relationshipsTable = pgTable(relationshipsTableName, relationshipColumns, (cols) => { - const result: Record = {}; - if (hasLocalizedRelationshipField) result.localeIdx = index('locale_idx').on(cols.locale); - return result; - }); + const result: Record = {} + if (hasLocalizedRelationshipField) result.localeIdx = index('locale_idx').on(cols.locale) + return result + }) - adapter.tables[relationshipsTableName] = relationshipsTable; + adapter.tables[relationshipsTableName] = relationshipsTable const relationshipsTableRelations = relations(relationshipsTable, ({ one }) => { const result: Record> = { parent: one(table, { - relationName: '_relationships', fields: [relationshipsTable.parent], references: [table.id], + relationName: '_relationships', }), - }; + } relationships.forEach((relationTo) => { - const relatedTableName = toSnakeCase(relationTo); - const idColumnName = `${relationTo}ID`; + const relatedTableName = toSnakeCase(relationTo) + const idColumnName = `${relationTo}ID` result[idColumnName] = one(adapter.tables[relatedTableName], { fields: [relationshipsTable[idColumnName]], references: [adapter.tables[relatedTableName].id], - }); - }); + }) + }) - return result; - }); + return result + }) - adapter.relations[`relations_${relationshipsTableName}`] = relationshipsTableRelations; + adapter.relations[`relations_${relationshipsTableName}`] = relationshipsTableRelations } } const tableRelations = relations(table, ({ many }) => { - const result: Record> = {}; + const result: Record> = {} arrayBlockRelations.forEach((val, key) => { - result[key] = many(adapter.tables[val]); - }); + result[key] = many(adapter.tables[val]) + }) if (hasLocalizedField) { - result._locales = many(localesTable); + result._locales = many(localesTable) } if (relationships.size && relationshipsTable) { result._relationships = many(relationshipsTable, { relationName: '_relationships', - }); + }) } - return result; - }); + return result + }) - adapter.relations[`relations_${formattedTableName}`] = tableRelations; + adapter.relations[`relations_${formattedTableName}`] = tableRelations - return { arrayBlockRelations }; -}; + return { arrayBlockRelations } +} diff --git a/packages/db-postgres/src/schema/createIndex.ts b/packages/db-postgres/src/schema/createIndex.ts index 75b797ebe..30e59cf9d 100644 --- a/packages/db-postgres/src/schema/createIndex.ts +++ b/packages/db-postgres/src/schema/createIndex.ts @@ -1,16 +1,17 @@ /* eslint-disable no-param-reassign */ -import { uniqueIndex, index } from 'drizzle-orm/pg-core'; -import { GenericColumn } from '../types'; +import { index, uniqueIndex } from 'drizzle-orm/pg-core' + +import type { GenericColumn } from '../types' type CreateIndexArgs = { - name: string columnName: string + name: string unique?: boolean } -export const createIndex = ({ name, columnName, unique }: CreateIndexArgs) => { +export const createIndex = ({ columnName, name, unique }: CreateIndexArgs) => { return (table: { [x: string]: GenericColumn }) => { - if (unique) return uniqueIndex(`${columnName}_idx`).on(table[name]); - return index(`${columnName}_idx`).on(table[name]); - }; -}; + if (unique) return uniqueIndex(`${columnName}_idx`).on(table[name]) + return index(`${columnName}_idx`).on(table[name]) + } +} diff --git a/packages/db-postgres/src/schema/parentIDColumnMap.ts b/packages/db-postgres/src/schema/parentIDColumnMap.ts index e6e3965b9..eba60e8d0 100644 --- a/packages/db-postgres/src/schema/parentIDColumnMap.ts +++ b/packages/db-postgres/src/schema/parentIDColumnMap.ts @@ -1,7 +1,7 @@ -import { integer, numeric, varchar } from 'drizzle-orm/pg-core'; +import { integer, numeric, varchar } from 'drizzle-orm/pg-core' export const parentIDColumnMap = { integer, - varchar, numeric, -}; + varchar, +} diff --git a/packages/db-postgres/src/schema/traverseFields.ts b/packages/db-postgres/src/schema/traverseFields.ts index 85e9e596b..500acaa22 100644 --- a/packages/db-postgres/src/schema/traverseFields.ts +++ b/packages/db-postgres/src/schema/traverseFields.ts @@ -1,23 +1,36 @@ /* eslint-disable no-param-reassign */ -import { integer, text, varchar, numeric, IndexBuilder, PgNumericBuilder, PgVarcharBuilder, jsonb } from 'drizzle-orm/pg-core'; -import { Field } from 'payload/types'; -import toSnakeCase from 'to-snake-case'; -import { fieldAffectsData } from 'payload/types'; -import { Relation, relations } from 'drizzle-orm'; -import { GenericColumns, PostgresAdapter } from '../types'; -import { createIndex } from './createIndex'; -import { buildTable } from './build'; -import { parentIDColumnMap } from './parentIDColumnMap'; -import { hasLocalesTable } from '../utilities/hasLocalesTable'; +import type { Relation } from 'drizzle-orm' +import type { IndexBuilder } from 'drizzle-orm/pg-core' +import type { Field } from 'payload/types' -type AnyPgColumnBuilder = any; // TODO: Fix this +import { relations } from 'drizzle-orm' +import { + PgNumericBuilder, + PgVarcharBuilder, + integer, + jsonb, + numeric, + text, + varchar, +} from 'drizzle-orm/pg-core' +import { fieldAffectsData } from 'payload/types' +import toSnakeCase from 'to-snake-case' + +import type { GenericColumns, PostgresAdapter } from '../types' + +import { hasLocalesTable } from '../utilities/hasLocalesTable' +import { buildTable } from './build' +import { createIndex } from './createIndex' +import { parentIDColumnMap } from './parentIDColumnMap' + +type AnyPgColumnBuilder = any // TODO: Fix this type Args = { adapter: PostgresAdapter arrayBlockRelations: Map buildRelationships: boolean - columns: Record columnPrefix?: string + columns: Record fieldPrefix?: string fields: Field[] indexes: Record IndexBuilder> @@ -48,33 +61,37 @@ export const traverseFields = ({ parentTableName, relationships, }: Args): Result => { - let hasLocalizedField = false; - let hasLocalizedRelationshipField = false; + let hasLocalizedField = false + let hasLocalizedRelationshipField = false - let parentIDColType = 'integer'; - if (columns.id instanceof PgNumericBuilder) parentIDColType = 'numeric'; - if (columns.id instanceof PgVarcharBuilder) parentIDColType = 'varchar'; + let parentIDColType = 'integer' + if (columns.id instanceof PgNumericBuilder) parentIDColType = 'numeric' + if (columns.id instanceof PgVarcharBuilder) parentIDColType = 'varchar' fields.forEach((field) => { - if ('name' in field && field.name === 'id') return; - let columnName: string; + if ('name' in field && field.name === 'id') return + let columnName: string - let targetTable = columns; - let targetIndexes = indexes; + let targetTable = columns + let targetIndexes = indexes if (fieldAffectsData(field)) { - columnName = `${columnPrefix || ''}${toSnakeCase(field.name)}`; + columnName = `${columnPrefix || ''}${toSnakeCase(field.name)}` // If field is localized, // add the column to the locale table instead of main table if (field.localized) { - hasLocalizedField = true; - targetTable = localesColumns; - targetIndexes = localesIndexes; + hasLocalizedField = true + targetTable = localesColumns + targetIndexes = localesIndexes } if (field.unique || field.index) { - targetIndexes[`${field.name}Idx`] = createIndex({ columnName, name: field.name, unique: field.unique }); + targetIndexes[`${field.name}Idx`] = createIndex({ + columnName, + name: field.name, + unique: field.unique, + }) } } @@ -85,59 +102,61 @@ export const traverseFields = ({ case 'textarea': { // TODO: handle hasMany // TODO: handle min / max length - targetTable[`${fieldPrefix || ''}${field.name}`] = varchar(columnName); - break; + targetTable[`${fieldPrefix || ''}${field.name}`] = varchar(columnName) + break } case 'number': { // TODO: handle hasMany // TODO: handle min / max - targetTable[`${fieldPrefix || ''}${field.name}`] = numeric(columnName); - break; + targetTable[`${fieldPrefix || ''}${field.name}`] = numeric(columnName) + break } case 'richText': case 'json': { - targetTable[`${fieldPrefix || ''}${field.name}`] = jsonb(columnName); - break; + targetTable[`${fieldPrefix || ''}${field.name}`] = jsonb(columnName) + break } case 'date': { - break; + break } case 'point': { - break; + break } case 'radio': { - break; + break } case 'select': { - break; + break } case 'array': { const baseColumns: Record = { _order: integer('_order').notNull(), - _parentID: parentIDColumnMap[parentIDColType]('_parent_id').references(() => adapter.tables[parentTableName].id).notNull(), - }; - - if (field.localized && adapter.payload.config.localization) { - baseColumns._locale = adapter.enums._locales('_locale').notNull(); + _parentID: parentIDColumnMap[parentIDColType]('_parent_id') + .references(() => adapter.tables[parentTableName].id) + .notNull(), } - const arrayTableName = `${newTableName}_${toSnakeCase(field.name)}`; + if (field.localized && adapter.payload.config.localization) { + baseColumns._locale = adapter.enums._locales('_locale').notNull() + } + + const arrayTableName = `${newTableName}_${toSnakeCase(field.name)}` const { arrayBlockRelations: subArrayBlockRelations } = buildTable({ adapter, baseColumns, fields: field.fields, tableName: arrayTableName, - }); + }) - arrayBlockRelations.set(`${fieldPrefix || ''}${field.name}`, arrayTableName); + arrayBlockRelations.set(`${fieldPrefix || ''}${field.name}`, arrayTableName) const arrayTableRelations = relations(adapter.tables[arrayTableName], ({ many, one }) => { const result: Record> = { @@ -145,37 +164,39 @@ export const traverseFields = ({ fields: [adapter.tables[arrayTableName]._parentID], references: [adapter.tables[parentTableName].id], }), - }; + } if (hasLocalesTable(field.fields)) { - result._locales = many(adapter.tables[`${arrayTableName}_locales`]); + result._locales = many(adapter.tables[`${arrayTableName}_locales`]) } subArrayBlockRelations.forEach((val, key) => { - result[key] = many(adapter.tables[val]); - }); + result[key] = many(adapter.tables[val]) + }) - return result; - }); + return result + }) - adapter.relations[`relations_${arrayTableName}`] = arrayTableRelations; + adapter.relations[`relations_${arrayTableName}`] = arrayTableRelations - break; + break } case 'blocks': { field.blocks.forEach((block) => { const baseColumns: Record = { _order: integer('_order').notNull(), + _parentID: parentIDColumnMap[parentIDColType]('_parent_id') + .references(() => adapter.tables[parentTableName].id) + .notNull(), _path: text('_path').notNull(), - _parentID: parentIDColumnMap[parentIDColType]('_parent_id').references(() => adapter.tables[parentTableName].id).notNull(), - }; - - if (field.localized && adapter.payload.config.localization) { - baseColumns._locale = adapter.enums._locales('_locale').notNull(); } - const blockTableName = `${newTableName}_${toSnakeCase(block.slug)}`; + if (field.localized && adapter.payload.config.localization) { + baseColumns._locale = adapter.enums._locales('_locale').notNull() + } + + const blockTableName = `${newTableName}_${toSnakeCase(block.slug)}` if (!adapter.tables[blockTableName]) { const { arrayBlockRelations: subArrayBlockRelations } = buildTable({ @@ -183,34 +204,37 @@ export const traverseFields = ({ baseColumns, fields: block.fields, tableName: blockTableName, - }); + }) - const blockTableRelations = relations(adapter.tables[blockTableName], ({ many, one }) => { - const result: Record> = { - _parentID: one(adapter.tables[parentTableName], { - fields: [adapter.tables[blockTableName]._parentID], - references: [adapter.tables[parentTableName].id], - }), - }; + const blockTableRelations = relations( + adapter.tables[blockTableName], + ({ many, one }) => { + const result: Record> = { + _parentID: one(adapter.tables[parentTableName], { + fields: [adapter.tables[blockTableName]._parentID], + references: [adapter.tables[parentTableName].id], + }), + } - if (hasLocalesTable(block.fields)) { - result._locales = many(adapter.tables[`${blockTableName}_locales`]); - } + if (hasLocalesTable(block.fields)) { + result._locales = many(adapter.tables[`${blockTableName}_locales`]) + } - subArrayBlockRelations.forEach((val, key) => { - result[key] = many(adapter.tables[val]); - }); + subArrayBlockRelations.forEach((val, key) => { + result[key] = many(adapter.tables[val]) + }) - return result; - }); + return result + }, + ) - adapter.relations[`relations_${blockTableName}`] = blockTableRelations; + adapter.relations[`relations_${blockTableName}`] = blockTableRelations } - arrayBlockRelations.set(`_blocks_${block.slug}`, blockTableName); - }); + arrayBlockRelations.set(`_blocks_${block.slug}`, blockTableName) + }) - break; + break } case 'group': { @@ -232,11 +256,11 @@ export const traverseFields = ({ newTableName: `${parentTableName}_${toSnakeCase(field.name)}`, parentTableName, relationships, - }); + }) - if (groupHasLocalizedField) hasLocalizedField = true; - if (groupHasLocalizedRelationshipField) hasLocalizedRelationshipField = true; - break; + if (groupHasLocalizedField) hasLocalizedField = true + if (groupHasLocalizedRelationshipField) hasLocalizedRelationshipField = true + break } case 'tabs': { @@ -259,15 +283,12 @@ export const traverseFields = ({ newTableName: `${parentTableName}_${toSnakeCase(tab.name)}`, parentTableName, relationships, - }); + }) - if (tabHasLocalizedField) hasLocalizedField = true; - if (tabHasLocalizedRelationshipField) hasLocalizedRelationshipField = true; + if (tabHasLocalizedField) hasLocalizedField = true + if (tabHasLocalizedRelationshipField) hasLocalizedRelationshipField = true } else { - ({ - hasLocalizedField, - hasLocalizedRelationshipField, - } = traverseFields({ + ;({ hasLocalizedField, hasLocalizedRelationshipField } = traverseFields({ adapter, arrayBlockRelations, buildRelationships, @@ -279,18 +300,15 @@ export const traverseFields = ({ newTableName: parentTableName, parentTableName, relationships, - })); + })) } - }); - break; + }) + break } case 'row': case 'collapsible': { - ({ - hasLocalizedField, - hasLocalizedRelationshipField, - } = traverseFields({ + ;({ hasLocalizedField, hasLocalizedRelationshipField } = traverseFields({ adapter, arrayBlockRelations, buildRelationships, @@ -302,27 +320,27 @@ export const traverseFields = ({ newTableName: parentTableName, parentTableName, relationships, - })); - break; + })) + break } case 'relationship': case 'upload': if (Array.isArray(field.relationTo)) { - field.relationTo.forEach((relation) => relationships.add(relation)); + field.relationTo.forEach((relation) => relationships.add(relation)) } else { - relationships.add(field.relationTo); + relationships.add(field.relationTo) } if (field.localized) { - hasLocalizedRelationshipField = true; + hasLocalizedRelationshipField = true } - break; + break default: - break; + break } - }); + }) - return { hasLocalizedField, hasLocalizedRelationshipField }; -}; + return { hasLocalizedField, hasLocalizedRelationshipField } +} diff --git a/packages/db-postgres/src/transform/read/index.ts b/packages/db-postgres/src/transform/read/index.ts index 26148e14f..9158df2be 100644 --- a/packages/db-postgres/src/transform/read/index.ts +++ b/packages/db-postgres/src/transform/read/index.ts @@ -1,16 +1,17 @@ /* eslint-disable no-param-reassign */ -import { Field } from 'payload/types'; -import { TypeWithID } from 'payload/types'; -import { SanitizedConfig } from 'payload/config'; -import { traverseFields } from './traverseFields'; -import { createRelationshipMap } from '../../utilities/createRelationshipMap'; -import { mergeLocales } from './mergeLocales'; -import { createBlocksMap } from '../../utilities/createBlocksMap'; +import type { SanitizedConfig } from 'payload/config' +import type { Field } from 'payload/types' +import type { TypeWithID } from 'payload/types' + +import { createBlocksMap } from '../../utilities/createBlocksMap' +import { createRelationshipMap } from '../../utilities/createRelationshipMap' +import { mergeLocales } from './mergeLocales' +import { traverseFields } from './traverseFields' type TransformArgs = { config: SanitizedConfig data: Record - fallbackLocale?: string | false + fallbackLocale?: false | string fields: Field[] locale?: string } @@ -24,16 +25,16 @@ export const transform = ({ fields, locale, }: TransformArgs): T => { - let relationships: Record[]> = {}; + let relationships: Record[]> = {} if ('_relationships' in data) { - relationships = createRelationshipMap(data._relationships); - delete data._relationships; + relationships = createRelationshipMap(data._relationships) + delete data._relationships } - const blocks = createBlocksMap(data); + const blocks = createBlocksMap(data) - const dataWithLocales = mergeLocales({ data, locale, fallbackLocale }); + const dataWithLocales = mergeLocales({ data, fallbackLocale, locale }) return traverseFields({ blocks, @@ -45,5 +46,5 @@ export const transform = ({ relationships, siblingData: dataWithLocales, table: dataWithLocales, - }); -}; + }) +} diff --git a/packages/db-postgres/src/transform/read/mergeLocales.ts b/packages/db-postgres/src/transform/read/mergeLocales.ts index abc8a1435..d55a0a647 100644 --- a/packages/db-postgres/src/transform/read/mergeLocales.ts +++ b/packages/db-postgres/src/transform/read/mergeLocales.ts @@ -1,7 +1,7 @@ /* eslint-disable no-param-reassign */ type MergeLocalesArgs = { data: Record - fallbackLocale?: string | false + fallbackLocale?: false | string locale?: string } @@ -14,57 +14,57 @@ export const mergeLocales = ({ }: MergeLocalesArgs): Record => { if (Array.isArray(data._locales)) { if (locale) { - const matchedLocale = data._locales.find((row) => row._locale === locale); + const matchedLocale = data._locales.find((row) => row._locale === locale) if (matchedLocale) { const merged = { ...data, ...matchedLocale, - }; + } - delete merged._parentID; - delete merged._locales; - delete merged._locale; - return merged; + delete merged._parentID + delete merged._locales + delete merged._locale + return merged } if (fallbackLocale) { - const matchedFallbackLocale = data._locales.find((row) => row._locale === fallbackLocale); + const matchedFallbackLocale = data._locales.find((row) => row._locale === fallbackLocale) if (matchedFallbackLocale) { const merged = { ...data, ...matchedFallbackLocale, - }; - delete merged._parentID; - delete merged._locales; - delete merged._locale; - return merged; + } + delete merged._parentID + delete merged._locales + delete merged._locale + return merged } } } const fieldLocales = data._locales.reduce((res, row) => { - const rowLocale = row._locale; - delete row._locale; + const rowLocale = row._locale + delete row._locale if (rowLocale) { Object.entries(row).forEach(([field, val]) => { - if (!res[field]) res[field] = {}; - res[field][rowLocale] = val; - }); + if (!res[field]) res[field] = {} + res[field][rowLocale] = val + }) } - return res; - }, {}); + return res + }, {}) - delete data._locales; + delete data._locales return { ...data, ...fieldLocales, - }; + } } - return data; -}; + return data +} diff --git a/packages/db-postgres/src/transform/read/traverseFields.ts b/packages/db-postgres/src/transform/read/traverseFields.ts index 61df0d549..ec91c8a68 100644 --- a/packages/db-postgres/src/transform/read/traverseFields.ts +++ b/packages/db-postgres/src/transform/read/traverseFields.ts @@ -1,10 +1,13 @@ /* eslint-disable no-param-reassign */ -import { fieldAffectsData } from 'payload/types'; -import { Field } from 'payload/types'; -import { SanitizedConfig } from 'payload/config'; -import { mergeLocales } from './mergeLocales'; -import { BlocksMap } from '../../utilities/createBlocksMap'; -import { transform } from '.'; +import type { SanitizedConfig } from 'payload/config' +import type { Field } from 'payload/types' + +import { fieldAffectsData } from 'payload/types' + +import type { BlocksMap } from '../../utilities/createBlocksMap' + +import { transform } from '.' +import { mergeLocales } from './mergeLocales' type TraverseFieldsArgs = { /** @@ -63,17 +66,17 @@ export const traverseFields = >({ siblingData, table, }: TraverseFieldsArgs): T => { - const sanitizedPath = path ? `${path}.` : path; + const sanitizedPath = path ? `${path}.` : path const formatted = fields.reduce((result, field) => { if (fieldAffectsData(field)) { - const fieldData = result[field.name]; + const fieldData = result[field.name] switch (field.type) { case 'array': if (Array.isArray(fieldData)) { result[field.name] = fieldData.map((row, i) => { - const dataWithLocales = mergeLocales({ data: row, locale, fallbackLocale }); + const dataWithLocales = mergeLocales({ data: row, fallbackLocale, locale }) return traverseFields({ blocks, @@ -85,20 +88,20 @@ export const traverseFields = >({ relationships, siblingData: dataWithLocales, table: dataWithLocales, - }); - }); + }) + }) } - break; + break case 'blocks': { - const blockFieldPath = `${sanitizedPath}${field.name}`; + const blockFieldPath = `${sanitizedPath}${field.name}` if (Array.isArray(blocks[blockFieldPath])) { result[field.name] = blocks[blockFieldPath].map((row, i) => { - delete row._order; - const dataWithLocales = mergeLocales({ data: row, locale, fallbackLocale }); - const block = field.blocks.find(({ slug }) => slug === row.blockType); + delete row._order + const dataWithLocales = mergeLocales({ data: row, fallbackLocale, locale }) + const block = field.blocks.find(({ slug }) => slug === row.blockType) if (block) { return traverseFields({ @@ -111,30 +114,32 @@ export const traverseFields = >({ relationships, siblingData: dataWithLocales, table: dataWithLocales, - }); + }) } - return {}; - }); + return {} + }) } - break; + break } case 'group': { const groupData: Record = { ...(typeof fieldData === 'object' ? fieldData : {}), - }; + } field.fields.forEach((subField) => { if (fieldAffectsData(subField)) { - const subFieldKey = `${sanitizedPath.replace(/[.]/g, '_')}${field.name}_${subField.name}`; + const subFieldKey = `${sanitizedPath.replace(/\./g, '_')}${field.name}_${ + subField.name + }` if (table[subFieldKey]) { - groupData[subField.name] = table[subFieldKey]; - delete table[subFieldKey]; + groupData[subField.name] = table[subFieldKey] + delete table[subFieldKey] } } - }); + }) result[field.name] = traverseFields>({ blocks, @@ -146,28 +151,32 @@ export const traverseFields = >({ relationships, siblingData: groupData, table, - }); + }) - break; + break } case 'relationship': { - const relationPathMatch = relationships[`${sanitizedPath}${field.name}`]; - if (!relationPathMatch) break; + const relationPathMatch = relationships[`${sanitizedPath}${field.name}`] + if (!relationPathMatch) break if (!field.hasMany) { - const relation = relationPathMatch[0]; + const relation = relationPathMatch[0] if (relation) { // Handle hasOne Poly if (Array.isArray(field.relationTo)) { - const matchedRelation = Object.entries(relation).find(([key, val]) => val !== null && !['order', 'id', 'parent'].includes(key)); + const matchedRelation = Object.entries(relation).find( + ([key, val]) => val !== null && !['id', 'order', 'parent'].includes(key), + ) if (matchedRelation) { - const relationTo = matchedRelation[0].replace('ID', ''); + const relationTo = matchedRelation[0].replace('ID', '') if (typeof matchedRelation[1] === 'object') { - const relatedCollection = config.collections.find(({ slug }) => slug === relationTo); + const relatedCollection = config.collections.find( + ({ slug }) => slug === relationTo, + ) if (relatedCollection) { const value = transform({ @@ -176,71 +185,79 @@ export const traverseFields = >({ fallbackLocale, fields: relatedCollection.fields, locale, - }); + }) result[field.name] = { relationTo, value, - }; + } } } else { result[field.name] = { relationTo, value: matchedRelation[1], - }; + } } } } else { // Handle hasOne - const relatedData = relation[`${field.relationTo}ID`]; + const relatedData = relation[`${field.relationTo}ID`] if (typeof relatedData === 'object' && relatedData !== null) { - const relatedCollection = config.collections.find(({ slug }) => slug === field.relationTo); + const relatedCollection = config.collections.find( + ({ slug }) => slug === field.relationTo, + ) result[field.name] = transform({ config, data: relatedData as Record, fallbackLocale, fields: relatedCollection.fields, locale, - }); + }) } else { - result[field.name] = relatedData; + result[field.name] = relatedData } } } } else { - const transformedRelations = [ - ...(Array.isArray(fieldData) ? fieldData : []), - ]; + const transformedRelations = [...(Array.isArray(fieldData) ? fieldData : [])] relationPathMatch.forEach((relation) => { // Handle hasMany if (!Array.isArray(field.relationTo)) { - const relatedCollection = config.collections.find(({ slug }) => slug === field.relationTo); - const relatedData = relation[`${field.relationTo}ID`]; + const relatedCollection = config.collections.find( + ({ slug }) => slug === field.relationTo, + ) + const relatedData = relation[`${field.relationTo}ID`] if (relatedData) { if (typeof relatedData === 'object' && relatedData !== null) { - transformedRelations.push(transform({ - config, - data: relatedData as Record, - fallbackLocale, - fields: relatedCollection.fields, - locale, - })); + transformedRelations.push( + transform({ + config, + data: relatedData as Record, + fallbackLocale, + fields: relatedCollection.fields, + locale, + }), + ) } else { - transformedRelations.push(relatedData); + transformedRelations.push(relatedData) } } } else { // Handle hasMany Poly - const matchedRelation = Object.entries(relation).find(([key, val]) => val !== null && !['order', 'parent', 'id'].includes(key)); + const matchedRelation = Object.entries(relation).find( + ([key, val]) => val !== null && !['id', 'order', 'parent'].includes(key), + ) if (matchedRelation) { - const relationTo = matchedRelation[0].replace('ID', ''); + const relationTo = matchedRelation[0].replace('ID', '') if (typeof matchedRelation[1] === 'object') { - const relatedCollection = config.collections.find(({ slug }) => slug === relationTo); + const relatedCollection = config.collections.find( + ({ slug }) => slug === relationTo, + ) if (relatedCollection) { const value = transform({ @@ -249,47 +266,47 @@ export const traverseFields = >({ fallbackLocale, fields: relatedCollection.fields, locale, - }); + }) transformedRelations.push({ relationTo, value, - }); + }) } } else { transformedRelations.push({ relationTo, value: matchedRelation[1], - }); + }) } } } - }); + }) - result[field.name] = transformedRelations; + result[field.name] = transformedRelations } - break; + break } case 'date': { if (fieldData instanceof Date) { - result[field.name] = fieldData.toISOString(); + result[field.name] = fieldData.toISOString() } - break; + break } default: { - break; + break } } - return result; + return result } - return siblingData; - }, siblingData); + return siblingData + }, siblingData) - return formatted as T; -}; + return formatted as T +} diff --git a/packages/db-postgres/src/transform/write/index.ts b/packages/db-postgres/src/transform/write/index.ts index 8d97929af..451334b9d 100644 --- a/packages/db-postgres/src/transform/write/index.ts +++ b/packages/db-postgres/src/transform/write/index.ts @@ -1,7 +1,9 @@ /* eslint-disable no-param-reassign */ -import { Field } from 'payload/types'; -import { traverseFields } from './traverseFields'; -import { RowToInsert } from './types'; +import type { Field } from 'payload/types' + +import type { RowToInsert } from './types' + +import { traverseFields } from './traverseFields' type Args = { data: Record @@ -21,12 +23,12 @@ export const transformForWrite = ({ // Split out the incoming data into the corresponding: // base row, locales, relationships, blocks, and arrays const rowToInsert: RowToInsert = { - row: {}, + arrays: {}, + blocks: {}, locale: {}, relationships: [], - blocks: {}, - arrays: {}, - }; + row: {}, + } // This function is responsible for building up the // above rowToInsert @@ -43,7 +45,7 @@ export const transformForWrite = ({ path, relationships: rowToInsert.relationships, row: rowToInsert.row, - }); + }) - return rowToInsert; -}; + return rowToInsert +} diff --git a/packages/db-postgres/src/transform/write/traverseFields.ts b/packages/db-postgres/src/transform/write/traverseFields.ts index 67f6e80e1..44201e8b9 100644 --- a/packages/db-postgres/src/transform/write/traverseFields.ts +++ b/packages/db-postgres/src/transform/write/traverseFields.ts @@ -1,9 +1,12 @@ /* eslint-disable no-param-reassign */ -import { Field } from 'payload/types'; -import toSnakeCase from 'to-snake-case'; -import { fieldAffectsData, valueIsValueWithRelation } from 'payload/types'; -import { ArrayRowToInsert, BlockRowToInsert } from './types'; -import { isArrayOfRows } from '../../utilities/isArrayOfRows'; +import type { Field } from 'payload/types' + +import { fieldAffectsData, valueIsValueWithRelation } from 'payload/types' +import toSnakeCase from 'to-snake-case' + +import type { ArrayRowToInsert, BlockRowToInsert } from './types' + +import { isArrayOfRows } from '../../utilities/isArrayOfRows' type Args = { arrays: { @@ -39,21 +42,23 @@ export const traverseFields = ({ row, }: Args) => { fields.forEach((field) => { - let targetRow = row; - let columnName = ''; - let fieldData; + let targetRow = row + let columnName = '' + let fieldData if (fieldAffectsData(field)) { - columnName = `${columnPrefix || ''}${field.name}`; - fieldData = data[field.name]; + columnName = `${columnPrefix || ''}${field.name}` + fieldData = data[field.name] if (field.localized) { - targetRow = localeRow; + targetRow = localeRow - if (typeof data[field.name] === 'object' - && data[field.name] !== null - && data[field.name][locale]) { - fieldData = data[field.name][locale]; + if ( + typeof data[field.name] === 'object' && + data[field.name] !== null && + data[field.name][locale] + ) { + fieldData = data[field.name][locale] } } } @@ -61,32 +66,32 @@ export const traverseFields = ({ switch (field.type) { case 'number': { // TODO: handle hasMany - targetRow[columnName] = fieldData; - break; + targetRow[columnName] = fieldData + break } case 'select': { - break; + break } case 'array': { if (isArrayOfRows(fieldData)) { - const arrayTableName = `${newTableName}_${toSnakeCase(field.name)}`; - if (!arrays[arrayTableName]) arrays[arrayTableName] = []; + const arrayTableName = `${newTableName}_${toSnakeCase(field.name)}` + if (!arrays[arrayTableName]) arrays[arrayTableName] = [] fieldData.forEach((arrayRow, i) => { const newRow: ArrayRowToInsert = { + arrays: {}, columnName, - row: { - _order: i + 1, - }, locale: { _locale: locale, }, - arrays: {}, - }; + row: { + _order: i + 1, + }, + } - if (field.localized) newRow.row._locale = locale; + if (field.localized) newRow.row._locale = locale traverseFields({ arrays: newRow.arrays, @@ -101,36 +106,36 @@ export const traverseFields = ({ path: `${path || ''}${field.name}.${i}.`, relationships, row: newRow.row, - }); + }) - arrays[arrayTableName].push(newRow); - }); + arrays[arrayTableName].push(newRow) + }) } - break; + break } case 'blocks': { if (isArrayOfRows(fieldData)) { fieldData.forEach((blockRow, i) => { - if (typeof blockRow.blockType !== 'string') return; - const matchedBlock = field.blocks.find(({ slug }) => slug === blockRow.blockType); - if (!matchedBlock) return; + if (typeof blockRow.blockType !== 'string') return + const matchedBlock = field.blocks.find(({ slug }) => slug === blockRow.blockType) + if (!matchedBlock) return - if (!blocks[blockRow.blockType]) blocks[blockRow.blockType] = []; + if (!blocks[blockRow.blockType]) blocks[blockRow.blockType] = [] const newRow: BlockRowToInsert = { arrays: {}, + locale: {}, row: { _order: i + 1, _path: `${path}${field.name}`, }, - locale: {}, - }; + } - if (field.localized) newRow.row._locale = locale; + if (field.localized) newRow.row._locale = locale - const blockTableName = `${newTableName}_${toSnakeCase(blockRow.blockType)}`; + const blockTableName = `${newTableName}_${toSnakeCase(blockRow.blockType)}` traverseFields({ arrays: newRow.arrays, @@ -145,13 +150,13 @@ export const traverseFields = ({ path: `${path || ''}${field.name}.${i}.`, relationships, row: newRow.row, - }); + }) - blocks[blockRow.blockType].push(newRow); - }); + blocks[blockRow.blockType].push(newRow) + }) } - break; + break } case 'group': { @@ -169,19 +174,19 @@ export const traverseFields = ({ path: `${path || ''}${field.name}.`, relationships, row, - }); + }) } - break; + break } case 'date': { if (typeof fieldData === 'string') { - const parsedDate = new Date(fieldData); - targetRow[columnName] = parsedDate; + const parsedDate = new Date(fieldData) + targetRow[columnName] = parsedDate } - break; + break } // case 'tabs': { @@ -247,35 +252,34 @@ export const traverseFields = ({ case 'relationship': case 'upload': { - const relations = Array.isArray(fieldData) ? fieldData : [fieldData]; + const relations = Array.isArray(fieldData) ? fieldData : [fieldData] relations.forEach((relation, i) => { const relationRow: Record = { path: `${path || ''}${field.name}`, - }; + } - if ('hasMany' in field && field.hasMany) relationRow.order = i + 1; - if (field.localized) relationRow.locale = locale; + if ('hasMany' in field && field.hasMany) relationRow.order = i + 1 + if (field.localized) relationRow.locale = locale if (Array.isArray(field.relationTo) && valueIsValueWithRelation(relation)) { - relationRow[`${relation.relationTo}ID`] = relation.value; - relationships.push(relationRow); + relationRow[`${relation.relationTo}ID`] = relation.value + relationships.push(relationRow) } else { - relationRow[`${field.relationTo}ID`] = relation; - if (relation) relationships.push(relationRow); + relationRow[`${field.relationTo}ID`] = relation + if (relation) relationships.push(relationRow) } - }); + }) - - break; + break } default: { if (typeof fieldData !== 'undefined') { - targetRow[columnName] = fieldData; + targetRow[columnName] = fieldData } - break; + break } } - }); -}; + }) +} diff --git a/packages/db-postgres/src/transform/write/types.ts b/packages/db-postgres/src/transform/write/types.ts index a3f6eb527..d8ff5bbe4 100644 --- a/packages/db-postgres/src/transform/write/types.ts +++ b/packages/db-postgres/src/transform/write/types.ts @@ -1,28 +1,28 @@ export type ArrayRowToInsert = { - columnName: string - row: Record, - locale: Record arrays: { [tableName: string]: ArrayRowToInsert[] } + columnName: string + locale: Record + row: Record } export type BlockRowToInsert = { - row: Record, - locale: Record arrays: { [tableName: string]: ArrayRowToInsert[] } + locale: Record + row: Record } export type RowToInsert = { - row: Record, - locale: Record, - relationships: Record[], - blocks: { - [blockType: string]: BlockRowToInsert[] - } arrays: { [tableName: string]: ArrayRowToInsert[] } + blocks: { + [blockType: string]: BlockRowToInsert[] + } + locale: Record + relationships: Record[] + row: Record } diff --git a/packages/db-postgres/src/types.ts b/packages/db-postgres/src/types.ts index a3ad768e0..b0459af93 100644 --- a/packages/db-postgres/src/types.ts +++ b/packages/db-postgres/src/types.ts @@ -1,20 +1,20 @@ -import { ColumnBaseConfig, ColumnDataType, Relation, Relations } from 'drizzle-orm'; -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { PgColumn, PgEnum, PgTableWithColumns } from 'drizzle-orm/pg-core'; -import { Payload } from 'payload'; -import { DatabaseAdapter } from 'payload/database'; -import { ClientConfig, PoolConfig } from 'pg'; +import type { ColumnBaseConfig, ColumnDataType, Relation, Relations } from 'drizzle-orm' +import type { NodePgDatabase } from 'drizzle-orm/node-postgres' +import type { PgColumn, PgEnum, PgTableWithColumns } from 'drizzle-orm/pg-core' +import type { Payload } from 'payload' +import type { DatabaseAdapter } from 'payload/database' +import type { ClientConfig, PoolConfig } from 'pg' export type DrizzleDB = NodePgDatabase> type BaseArgs = { - migrationDir?: string; - migrationName?: string; + migrationDir?: string + migrationName?: string } type ClientArgs = { /** Client connection options for the Node package `pg` */ - client?: ClientConfig | string | false + client?: ClientConfig | false | string } & BaseArgs type PoolArgs = { @@ -24,26 +24,33 @@ type PoolArgs = { export type Args = ClientArgs | PoolArgs -export type GenericColumn = PgColumn, Record> +export type GenericColumn = PgColumn< + ColumnBaseConfig, + Record +> export type GenericColumns = { [x: string]: GenericColumn } export type GenericTable = PgTableWithColumns<{ - name: string, schema: undefined, columns: GenericColumns, dialect: string + columns: GenericColumns + dialect: string + name: string + schema: undefined }> export type GenericEnum = PgEnum<[string, ...string[]]> export type GenericRelation = Relations>> -export type PostgresAdapter = DatabaseAdapter & Args & { - db: DrizzleDB - enums: Record - relations: Record - tables: Record - schema: Record -} +export type PostgresAdapter = DatabaseAdapter & + Args & { + db: DrizzleDB + enums: Record + relations: Record + schema: Record + tables: Record + } export type PostgresAdapterResult = (args: { payload: Payload }) => PostgresAdapter diff --git a/packages/db-postgres/src/update/index.ts b/packages/db-postgres/src/update/index.ts index a6faa7e35..9d9a2892f 100644 --- a/packages/db-postgres/src/update/index.ts +++ b/packages/db-postgres/src/update/index.ts @@ -1,8 +1,10 @@ -import { UpdateOne } from 'payload/database'; -import toSnakeCase from 'to-snake-case'; -import { SQL } from 'drizzle-orm'; -import buildQuery from '../queries/buildQuery'; -import { upsertRow } from '../upsertRow'; +import type { SQL } from 'drizzle-orm' +import type { UpdateOne } from 'payload/database' + +import toSnakeCase from 'to-snake-case' + +import buildQuery from '../queries/buildQuery' +import { upsertRow } from '../upsertRow' export const updateOne: UpdateOne = async function updateOne({ collection: collectionSlug, @@ -13,9 +15,9 @@ export const updateOne: UpdateOne = async function updateOne({ req, where, }) { - const collection = this.payload.collections[collectionSlug].config; + const collection = this.payload.collections[collectionSlug].config - let query: SQL; + let query: SQL if (where) { query = await buildQuery({ @@ -23,7 +25,7 @@ export const updateOne: UpdateOne = async function updateOne({ collectionSlug, locale, where, - }); + }) } const result = await upsertRow({ @@ -36,7 +38,7 @@ export const updateOne: UpdateOne = async function updateOne({ operation: 'update', tableName: toSnakeCase(collectionSlug), where: query, - }); + }) - return result; -}; + return result +} diff --git a/packages/db-postgres/src/upsertRow/index.ts b/packages/db-postgres/src/upsertRow/index.ts index 9428a734e..d0781745c 100644 --- a/packages/db-postgres/src/upsertRow/index.ts +++ b/packages/db-postgres/src/upsertRow/index.ts @@ -1,10 +1,12 @@ /* eslint-disable no-param-reassign */ -import { and, eq, inArray } from 'drizzle-orm'; -import { transform } from '../transform/read'; -import { BlockRowToInsert } from '../transform/write/types'; -import { insertArrays } from '../insertArrays'; -import { transformForWrite } from '../transform/write'; -import { Args } from './types'; +import { and, eq, inArray } from 'drizzle-orm' + +import type { BlockRowToInsert } from '../transform/write/types' +import type { Args } from './types' + +import { insertArrays } from '../insertArrays' +import { transform } from '../transform/read' +import { transformForWrite } from '../transform/write' export const upsertRow = async ({ adapter, @@ -27,189 +29,208 @@ export const upsertRow = async ({ locale, path, tableName, - }); + }) // First, we insert the main row - let insertedRow: Record; + let insertedRow: Record if (operation === 'update') { - const target = upsertTarget || adapter.tables[tableName].id; + const target = upsertTarget || adapter.tables[tableName].id if (id) { - rowToInsert.row.id = id; - [insertedRow] = await adapter.db.insert(adapter.tables[tableName]) + rowToInsert.row.id = id + ;[insertedRow] = await adapter.db + .insert(adapter.tables[tableName]) .values(rowToInsert.row) - .onConflictDoUpdate({ target, set: rowToInsert.row }) - .returning(); + .onConflictDoUpdate({ set: rowToInsert.row, target }) + .returning() } else { - [insertedRow] = await adapter.db.insert(adapter.tables[tableName]) + ;[insertedRow] = await adapter.db + .insert(adapter.tables[tableName]) .values(rowToInsert.row) - .onConflictDoUpdate({ target, set: rowToInsert.row, where }) - .returning(); + .onConflictDoUpdate({ set: rowToInsert.row, target, where }) + .returning() } } else { - [insertedRow] = await adapter.db.insert(adapter.tables[tableName]) - .values(rowToInsert.row).returning(); + ;[insertedRow] = await adapter.db + .insert(adapter.tables[tableName]) + .values(rowToInsert.row) + .returning() } - - let localeToInsert: Record; - const relationsToInsert: Record[] = []; - const blocksToInsert: { [blockType: string]: BlockRowToInsert[] } = {}; + let localeToInsert: Record + const relationsToInsert: Record[] = [] + const blocksToInsert: { [blockType: string]: BlockRowToInsert[] } = {} // Maintain a list of promises to run locale, blocks, and relationships // all in parallel - const promises = []; + const promises = [] // If there is a locale row with data, add the parent and locale if (Object.keys(rowToInsert.locale).length > 0) { - rowToInsert.locale._parentID = insertedRow.id; - rowToInsert.locale._locale = locale; - localeToInsert = rowToInsert.locale; + rowToInsert.locale._parentID = insertedRow.id + rowToInsert.locale._locale = locale + localeToInsert = rowToInsert.locale } // If there are relationships, add parent to each if (rowToInsert.relationships.length > 0) { rowToInsert.relationships.forEach((relation) => { - relation.parent = insertedRow.id; - relationsToInsert.push(relation); - }); + relation.parent = insertedRow.id + relationsToInsert.push(relation) + }) } // If there are blocks, add parent to each, and then // store by table name and rows Object.keys(rowToInsert.blocks).forEach((blockName) => { rowToInsert.blocks[blockName].forEach((blockRow) => { - blockRow.row._parentID = insertedRow.id; - if (!blocksToInsert[blockName]) blocksToInsert[blockName] = []; - blocksToInsert[blockName].push(blockRow); - }); - }); + blockRow.row._parentID = insertedRow.id + if (!blocksToInsert[blockName]) blocksToInsert[blockName] = [] + blocksToInsert[blockName].push(blockRow) + }) + }) // ////////////////////////////////// // INSERT LOCALES // ////////////////////////////////// - let insertedLocaleRow: Record; + let insertedLocaleRow: Record if (localeToInsert) { - const localeTableName = adapter.tables[`${tableName}_locales`]; + const localeTableName = adapter.tables[`${tableName}_locales`] promises.push(async () => { if (operation === 'update') { - [insertedLocaleRow] = await adapter.db.insert(localeTableName) + ;[insertedLocaleRow] = await adapter.db + .insert(localeTableName) .values(localeToInsert) .onConflictDoUpdate({ - target: [localeTableName._locale, localeTableName._parentID], set: localeToInsert, + target: [localeTableName._locale, localeTableName._parentID], }) - .returning(); + .returning() } else { - [insertedLocaleRow] = await adapter.db.insert(localeTableName) - .values(localeToInsert).returning(); + ;[insertedLocaleRow] = await adapter.db + .insert(localeTableName) + .values(localeToInsert) + .returning() } - }); + }) } // ////////////////////////////////// // INSERT RELATIONSHIPS // ////////////////////////////////// - let insertedRelationshipRows: Record[]; + let insertedRelationshipRows: Record[] if (relationsToInsert.length > 0) { promises.push(async () => { if (operation === 'update') { // Delete any relationship rows for parent ID and paths that have been updated // prior to recreating them - const localizedPathsToDelete = new Set(); - const pathsToDelete = new Set(); + const localizedPathsToDelete = new Set() + const pathsToDelete = new Set() relationsToInsert.forEach((relation) => { if (typeof relation.path === 'string') { if (typeof relation.locale === 'string') { - localizedPathsToDelete.add(relation.path); + localizedPathsToDelete.add(relation.path) } else { - pathsToDelete.add(relation.path); + pathsToDelete.add(relation.path) } } - }); + }) if (localizedPathsToDelete.size > 0) { - await adapter.db.delete(adapter.tables[`${tableName}_relationships`]) + await adapter.db + .delete(adapter.tables[`${tableName}_relationships`]) .where( and( eq(adapter.tables[`${tableName}_relationships`].parent, insertedRow.id), - inArray(adapter.tables[`${tableName}_relationships`].path, [localizedPathsToDelete]), + inArray(adapter.tables[`${tableName}_relationships`].path, [ + localizedPathsToDelete, + ]), eq(adapter.tables[`${tableName}_relationships`].locale, locale), ), - ); + ) } if (pathsToDelete.size > 0) { - await adapter.db.delete(adapter.tables[`${tableName}_relationships`]) + await adapter.db + .delete(adapter.tables[`${tableName}_relationships`]) .where( and( eq(adapter.tables[`${tableName}_relationships`].parent, insertedRow.id), - inArray(adapter.tables[`${tableName}_relationships`].path, Array.from(pathsToDelete)), + inArray( + adapter.tables[`${tableName}_relationships`].path, + Array.from(pathsToDelete), + ), ), - ); + ) } } - insertedRelationshipRows = await adapter.db.insert(adapter.tables[`${tableName}_relationships`]) - .values(relationsToInsert).returning(); - }); + insertedRelationshipRows = await adapter.db + .insert(adapter.tables[`${tableName}_relationships`]) + .values(relationsToInsert) + .returning() + }) } // ////////////////////////////////// // INSERT BLOCKS // ////////////////////////////////// - const insertedBlockRows: Record[]> = {}; + const insertedBlockRows: Record[]> = {} Object.entries(blocksToInsert).forEach(([blockName, blockRows]) => { // For each block, push insert into promises to run parallel promises.push(async () => { - insertedBlockRows[blockName] = await adapter.db.insert(adapter.tables[`${tableName}_${blockName}`]) - .values(blockRows.map(({ row }) => row)).returning(); + insertedBlockRows[blockName] = await adapter.db + .insert(adapter.tables[`${tableName}_${blockName}`]) + .values(blockRows.map(({ row }) => row)) + .returning() insertedBlockRows[blockName].forEach((row, i) => { - delete row._parentID; - blockRows[i].row = row; - }); + delete row._parentID + blockRows[i].row = row + }) - const blockLocaleIndexMap: number[] = []; + const blockLocaleIndexMap: number[] = [] const blockLocaleRowsToInsert = blockRows.reduce((acc, blockRow, i) => { if (Object.keys(blockRow.locale).length > 0) { - blockRow.locale._parentID = blockRow.row.id; - blockRow.locale._locale = locale; - acc.push(blockRow.locale); - blockLocaleIndexMap.push(i); - return acc; + blockRow.locale._parentID = blockRow.row.id + blockRow.locale._locale = locale + acc.push(blockRow.locale) + blockLocaleIndexMap.push(i) + return acc } - return acc; - }, []); + return acc + }, []) if (blockLocaleRowsToInsert.length > 0) { - const insertedBlockLocaleRows = await adapter.db.insert(adapter.tables[`${tableName}_${blockName}_locales`]) - .values(blockLocaleRowsToInsert).returning(); + const insertedBlockLocaleRows = await adapter.db + .insert(adapter.tables[`${tableName}_${blockName}_locales`]) + .values(blockLocaleRowsToInsert) + .returning() insertedBlockLocaleRows.forEach((blockLocaleRow, i) => { - delete blockLocaleRow._parentID; - insertedBlockRows[blockName][blockLocaleIndexMap[i]]._locales = [blockLocaleRow]; - }); + delete blockLocaleRow._parentID + insertedBlockRows[blockName][blockLocaleIndexMap[i]]._locales = [blockLocaleRow] + }) } await insertArrays({ adapter, arrays: blockRows.map(({ arrays }) => arrays), parentRows: insertedBlockRows[blockName], - }); - }); - }); + }) + }) + }) // ////////////////////////////////// // INSERT ARRAYS RECURSIVELY @@ -220,21 +241,21 @@ export const upsertRow = async ({ adapter, arrays: [rowToInsert.arrays], parentRows: [insertedRow], - }); - }); + }) + }) - await Promise.all(promises.map((promise) => promise())); + await Promise.all(promises.map((promise) => promise())) // ////////////////////////////////// // TRANSFORM DATA // ////////////////////////////////// - if (insertedLocaleRow) insertedRow._locales = [insertedLocaleRow]; - if (insertedRelationshipRows?.length > 0) insertedRow._relationships = insertedRelationshipRows; + if (insertedLocaleRow) insertedRow._locales = [insertedLocaleRow] + if (insertedRelationshipRows?.length > 0) insertedRow._relationships = insertedRelationshipRows Object.entries(insertedBlockRows).forEach(([blockName, blocks]) => { - if (blocks.length > 0) insertedRow[`_blocks_${blockName}`] = blocks; - }); + if (blocks.length > 0) insertedRow[`_blocks_${blockName}`] = blocks + }) const result = transform({ config: adapter.payload.config, @@ -242,7 +263,7 @@ export const upsertRow = async ({ fallbackLocale, fields, locale, - }); + }) - return result; -}; + return result +} diff --git a/packages/db-postgres/src/upsertRow/types.ts b/packages/db-postgres/src/upsertRow/types.ts index cb092b62c..57e02d824 100644 --- a/packages/db-postgres/src/upsertRow/types.ts +++ b/packages/db-postgres/src/upsertRow/types.ts @@ -1,11 +1,12 @@ -import { Field } from 'payload/types'; -import { SQL } from 'drizzle-orm'; -import { GenericColumn, PostgresAdapter } from '../types'; +import type { SQL } from 'drizzle-orm' +import type { Field } from 'payload/types' + +import type { GenericColumn, PostgresAdapter } from '../types' type BaseArgs = { adapter: PostgresAdapter data: Record - fallbackLocale?: string | false + fallbackLocale?: false | string fields: Field[] locale: string path?: string @@ -13,17 +14,17 @@ type BaseArgs = { } type CreateArgs = BaseArgs & { - upsertTarget?: never - where?: never id?: never operation: 'create' + upsertTarget?: never + where?: never } type UpdateArgs = BaseArgs & { - upsertTarget?: GenericColumn + id?: number | string operation: 'update' + upsertTarget?: GenericColumn where?: SQL - id?: string | number } export type Args = CreateArgs | UpdateArgs diff --git a/packages/db-postgres/src/utilities/createBlocksMap.ts b/packages/db-postgres/src/utilities/createBlocksMap.ts index e68a4d03f..c09df30f8 100644 --- a/packages/db-postgres/src/utilities/createBlocksMap.ts +++ b/packages/db-postgres/src/utilities/createBlocksMap.ts @@ -4,39 +4,39 @@ export type BlocksMap = { } export const createBlocksMap = (data: Record): BlocksMap => { - const blocksMap: BlocksMap = {}; + const blocksMap: BlocksMap = {} Object.entries(data).forEach(([key, rows]) => { if (key.startsWith('_blocks_') && Array.isArray(rows)) { - const blockType = key.replace('_blocks_', ''); + const blockType = key.replace('_blocks_', '') rows.forEach((row) => { if ('_path' in row) { - if (!(row._path in blocksMap)) blocksMap[row._path] = []; + if (!(row._path in blocksMap)) blocksMap[row._path] = [] - row.blockType = blockType; - blocksMap[row._path].push(row); + row.blockType = blockType + blocksMap[row._path].push(row) - delete row._locale; - delete row._path; + delete row._locale + delete row._path } - }); + }) - delete data[key]; + delete data[key] } - }); + }) Object.entries(blocksMap).reduce((sortedBlocksMap, [path, blocks]) => { sortedBlocksMap[path] = blocks.sort((a, b) => { if (typeof a._order === 'number' && typeof b._order === 'number') { - return a._order - b._order; + return a._order - b._order } - return 0; - }); + return 0 + }) - return sortedBlocksMap; - }, {}); + return sortedBlocksMap + }, {}) - return blocksMap; -}; + return blocksMap +} diff --git a/packages/db-postgres/src/utilities/createRelationshipMap.ts b/packages/db-postgres/src/utilities/createRelationshipMap.ts index e2f1cca16..2ea7b2766 100644 --- a/packages/db-postgres/src/utilities/createRelationshipMap.ts +++ b/packages/db-postgres/src/utilities/createRelationshipMap.ts @@ -1,22 +1,24 @@ // Flatten relationships to object with path keys // for easier retrieval -export const createRelationshipMap = (rawRelationships: unknown): Record[]> => { - let relationships = {}; +export const createRelationshipMap = ( + rawRelationships: unknown, +): Record[]> => { + let relationships = {} if (Array.isArray(rawRelationships)) { relationships = rawRelationships.reduce((res, relation) => { const formattedRelation = { ...relation, - }; + } - delete formattedRelation.path; + delete formattedRelation.path - if (!res[relation.path]) res[relation.path] = []; - res[relation.path].push(formattedRelation); + if (!res[relation.path]) res[relation.path] = [] + res[relation.path].push(formattedRelation) - return res; - }, {}); + return res + }, {}) } - return relationships; -}; + return relationships +} diff --git a/packages/db-postgres/src/utilities/hasLocalesTable.ts b/packages/db-postgres/src/utilities/hasLocalesTable.ts index 4c0ccc3a0..ef4963124 100644 --- a/packages/db-postgres/src/utilities/hasLocalesTable.ts +++ b/packages/db-postgres/src/utilities/hasLocalesTable.ts @@ -1,11 +1,12 @@ -import { fieldAffectsData, fieldHasSubFields } from 'payload/types'; -import { Field } from 'payload/types'; +import type { Field } from 'payload/types' + +import { fieldAffectsData, fieldHasSubFields } from 'payload/types' export const hasLocalesTable = (fields: Field[]): boolean => { return fields.some((field) => { - if (fieldAffectsData(field) && field.localized) return true; - if (fieldHasSubFields(field) && field.type !== 'array') return hasLocalesTable(field.fields); - if (field.type === 'tabs') return field.tabs.some((tab) => hasLocalesTable(tab.fields)); - return false; - }); -}; + if (fieldAffectsData(field) && field.localized) return true + if (fieldHasSubFields(field) && field.type !== 'array') return hasLocalesTable(field.fields) + if (field.type === 'tabs') return field.tabs.some((tab) => hasLocalesTable(tab.fields)) + return false + }) +} diff --git a/packages/db-postgres/src/utilities/isArrayOfRows.ts b/packages/db-postgres/src/utilities/isArrayOfRows.ts index 3390d6773..ef8528bfa 100644 --- a/packages/db-postgres/src/utilities/isArrayOfRows.ts +++ b/packages/db-postgres/src/utilities/isArrayOfRows.ts @@ -1,3 +1,3 @@ export function isArrayOfRows(data: unknown): data is Record[] { - return Array.isArray(data); + return Array.isArray(data) } diff --git a/packages/db-postgres/src/webpack.ts b/packages/db-postgres/src/webpack.ts index 17f8a371e..e60d64609 100644 --- a/packages/db-postgres/src/webpack.ts +++ b/packages/db-postgres/src/webpack.ts @@ -1,15 +1,16 @@ -import path from 'path'; -import type { Webpack } from 'payload/database'; +import type { Webpack } from 'payload/database' + +import path from 'path' export const webpack: Webpack = (config) => { return { ...config, resolve: { - ...config.resolve || {}, + ...(config.resolve || {}), alias: { - ...config.resolve?.alias || {}, + ...(config.resolve?.alias || {}), [path.resolve(__dirname, './index')]: path.resolve(__dirname, 'mock'), }, }, - }; -}; + } +} diff --git a/packages/db-postgres/tsconfig.json b/packages/db-postgres/tsconfig.json index d35bd57fa..27b3e1694 100644 --- a/packages/db-postgres/tsconfig.json +++ b/packages/db-postgres/tsconfig.json @@ -1,16 +1,16 @@ { "compilerOptions": { - "target": "ESNext", - "module": "NodeNext", - "moduleResolution": "NodeNext" /* Required for exports to work */, "composite": true, "declaration": true /* Generates corresponding '.d.ts' file. */, - "rootDir": "./src" /* Specify the root folder within your source files. */, - "outDir": "./dist" /* Specify an output folder for all emitted files. */, "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + "module": "NodeNext", + "moduleResolution": "NodeNext" /* Required for exports to work */, + "outDir": "./dist" /* Specify an output folder for all emitted files. */, "resolveJsonModule": true, - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "rootDir": "./src" /* Specify the root folder within your source files. */, + "skipLibCheck": true /* Skip type checking all .d.ts files. */, + "target": "ESNext" }, "exclude": [ "dist", diff --git a/packages/payload/.eslintrc.cjs b/packages/payload/.eslintrc.cjs index 68efd14d7..2fca0c29e 100644 --- a/packages/payload/.eslintrc.cjs +++ b/packages/payload/.eslintrc.cjs @@ -13,6 +13,10 @@ module.exports = { 'bin-esm.mjs', 'esm-loader.mjs', 'esm-loader-playwright.mjs', + '*.json', + '*.md', + '*.yml', + '*.yaml', ], }, { diff --git a/packages/payload/bin.js b/packages/payload/bin.js index 86541f415..581cf3a89 100755 --- a/packages/payload/bin.js +++ b/packages/payload/bin.js @@ -1,2 +1,2 @@ #!/usr/bin/env node -require('./dist/bin'); +require('./dist/bin') diff --git a/packages/payload/jest.components.config.js b/packages/payload/jest.components.config.js index 783e75409..6de1ac19a 100644 --- a/packages/payload/jest.components.config.js +++ b/packages/payload/jest.components.config.js @@ -1,18 +1,16 @@ module.exports = { - verbose: true, - testTimeout: 15000, - testEnvironment: 'jsdom', - testRegex: '(/src/admin/.*\\.(test|spec))\\.[jt]sx?$', + moduleNameMapper: { + '\\.(css|scss)$': '/src/bundlers/mocks/emptyModule.js', + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/src/bundlers/mocks/fileMock.js', + }, setupFilesAfterEnv: ['/test/componentsSetup.js'], + testEnvironment: 'jsdom', + testPathIgnorePatterns: ['node_modules', 'dist'], + testRegex: '(/src/admin/.*\\.(test|spec))\\.[jt]sx?$', + testTimeout: 15000, transform: { '^.+\\.(t|j)sx?$': ['@swc/jest'], }, - testPathIgnorePatterns: [ - 'node_modules', - 'dist', - ], - moduleNameMapper: { - '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/src/bundlers/mocks/fileMock.js', - '\\.(css|scss)$': '/src/bundlers/mocks/emptyModule.js', - }, -}; + verbose: true, +} diff --git a/packages/payload/jest.config.js b/packages/payload/jest.config.js index 127842e15..9791b1e6b 100644 --- a/packages/payload/jest.config.js +++ b/packages/payload/jest.config.js @@ -1,17 +1,15 @@ module.exports = { - verbose: true, + globalSetup: './test/jest.setup.ts', + moduleNameMapper: { + '\\.(css|scss)$': '/src/bundlers/mocks/emptyModule.js', + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/src/bundlers/mocks/fileMock.js', + }, testEnvironment: 'node', - testMatch: [ - '**/src/**/*.spec.ts', - '**/test/**/*int.spec.ts', - ], + testMatch: ['**/src/**/*.spec.ts', '**/test/**/*int.spec.ts'], + testTimeout: 90000, transform: { '^.+\\.(t|j)sx?$': ['@swc/jest'], }, - globalSetup: './test/jest.setup.ts', - testTimeout: 90000, - moduleNameMapper: { - '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/src/bundlers/mocks/fileMock.js', - '\\.(css|scss)$': '/src/bundlers/mocks/emptyModule.js', - }, -}; + verbose: true, +} diff --git a/packages/payload/nodemon.json b/packages/payload/nodemon.json index e8a71a42d..e2e2f2802 100644 --- a/packages/payload/nodemon.json +++ b/packages/payload/nodemon.json @@ -1,4 +1,6 @@ { + "exec": "ts-node ./test/dev.ts", + "ext": "ts,js,json", "ignore": [ ".git", "node_modules", @@ -7,7 +9,5 @@ "src/**/*.spec.ts", "test/**/payload-types.ts" ], - "watch": ["src/**/*.ts", "test/", "packages/**/*.ts"], - "ext": "ts,js,json", - "exec": "ts-node ./test/dev.ts" + "watch": ["src/**/*.ts", "test/", "packages/**/*.ts"] } diff --git a/packages/payload/package.json b/packages/payload/package.json index 383f863df..0b7b02e5e 100644 --- a/packages/payload/package.json +++ b/packages/payload/package.json @@ -1,126 +1,16 @@ { - "name": "payload", - "version": "1.14.0", - "description": "Node, React and MongoDB Headless CMS and Application Framework", - "license": "MIT", - "engines": { - "node": ">=14", - "pnpm": ">=8" - }, "author": { "email": "info@payloadcms.com", "name": "Payload", "url": "https://payloadcms.com" }, - "maintainers": [ - { - "name": "Payload", - "email": "info@payloadcms.com", - "url": "https://payloadcms.com" - } - ], - "repository": { - "type": "git", - "url": "https://github.com/payloadcms/payload.git" - }, - "homepage": "https://payloadcms.com", - "main": "./src/index.ts", - "types": "./src/index.ts", - "publishConfig": { - "registry": "https://registry.npmjs.org/", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "module": "./dist/index.js", - "access": "public", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - }, - "./*": { - "types": "./dist/exports/*.d.ts", - "default": "./dist/exports/*.js" - }, - "./scss": { - "default": "./dist/exports/scss.scss" - } - } - }, - "exports": { - ".": { - "types": "./src/index.ts", - "import": "./src/index.ts", - "require": "./src/index.ts" - }, - "./*": { - "types": "./src/exports/*.ts", - "import": "./src/exports/*.ts", - "require": "./src/exports/*.ts" - }, - "./scss": { - "import": "./src/exports/scss.scss", - "require": "./src/exports/scss.scss" - } - }, - "sideEffects": false, "bin": { "payload": "bin.js" }, - "scripts": { - "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/", - "build:tsc": "tsc -p tsconfig.build.json", - "build:components": "webpack --config dist/bundlers/webpack/components.config.js", - "build": "pnpm copyfiles && pnpm build:tsc && pnpm build:components", - "build:watch": "nodemon --watch 'src/**' --ext 'ts,tsx' --exec \"pnpm build:tsc\"", - "dev": "nodemon", - "dev:postgres": "cross-env PAYLOAD_DATABASE=postgres nodemon", - "dev:generate-types": "ts-node -T ./test/generateTypes.ts", - "dev:generate-graphql-schema": "ts-node -T ./test/generateGraphQLSchema.ts", - "pretest": "pnpm build", - "test": "pnpm test:int && pnpm test:components && pnpm test:e2e", - "test:int": "cross-env DISABLE_LOGGING=true jest --forceExit --detectOpenHandles", - "test:e2e": "ts-node -T ./test/runE2E.ts", - "test:e2e:headed": "cross-env DISABLE_LOGGING=true playwright test --headed", - "test:e2e:debug": "cross-env PWDEBUG=1 DISABLE_LOGGING=true playwright test", - "test:components": "cross-env jest --config=jest.components.config.js", - "translateNewKeys": "ts-node -T ./scripts/translateNewKeys.ts", - "clean:cache": "rimraf node_modules/.cache", - "clean": "rimraf dist", - "release:patch": "release-it patch", - "release:minor": "release-it minor", - "release:major": "release-it major", - "release:beta": "release-it pre --preReleaseId=beta --npm.tag=beta --config .release-it.pre.json", - "release:canary": "release-it pre --preReleaseId=canary --npm.tag=canary --config .release-it.pre.json", - "fix": "eslint \"src/**/*.ts\" --fix", - "lint": "eslint \"src/**/*.ts\"" - }, "bugs": { "url": "https://github.com/payloadcms/payload" }, - "keywords": [ - "payload", - "cms", - "content management", - "framework", - "typescript", - "javascript", - "node", - "express", - "headless", - "graphQL", - "restful", - "access control", - "dashboard", - "admin panel", - "api", - "MongoDB", - "self hosted", - "react", - "auth" - ], "dependencies": { - "@payloadcms/db-postgres": "workspace:*", - "@payloadcms/db-mongodb": "workspace:*", "@date-io/date-fns": "2.16.0", "@dnd-kit/core": "6.0.8", "@dnd-kit/sortable": "7.0.2", @@ -128,6 +18,8 @@ "@faceless-ui/scroll-info": "1.3.0", "@faceless-ui/window-info": "2.1.1", "@monaco-editor/react": "4.5.1", + "@payloadcms/db-mongodb": "workspace:*", + "@payloadcms/db-postgres": "workspace:*", "@swc/core": "1.3.76", "@swc/register": "0.1.10", "@types/sharp": "0.31.1", @@ -231,7 +123,9 @@ "webpack-dev-middleware": "6.0.1", "webpack-hot-middleware": "2.25.4" }, + "description": "Node, React and MongoDB Headless CMS and Application Framework", "devDependencies": { + "@payloadcms/eslint-config": "workspace:*", "@playwright/test": "1.37.1", "@release-it/conventional-changelog": "7.0.0", "@swc/jest": "0.2.29", @@ -307,8 +201,27 @@ "shelljs": "0.8.5", "slash": "3.0.0", "terser": "5.19.2", - "ts-node": "10.9.1", - "@payloadcms/eslint-config": "workspace:*" + "ts-node": "10.9.1" + }, + "engines": { + "node": ">=14", + "pnpm": ">=8" + }, + "exports": { + ".": { + "import": "./src/index.ts", + "require": "./src/index.ts", + "types": "./src/index.ts" + }, + "./*": { + "import": "./src/exports/*.ts", + "require": "./src/exports/*.ts", + "types": "./src/exports/*.ts" + }, + "./scss": { + "import": "./src/exports/scss.scss", + "require": "./src/exports/scss.scss" + } }, "files": [ "bin.js", @@ -320,5 +233,92 @@ "*.d.ts", "!jest.config.js", "!jest.components.config.js" - ] + ], + "homepage": "https://payloadcms.com", + "keywords": [ + "payload", + "cms", + "content management", + "framework", + "typescript", + "javascript", + "node", + "express", + "headless", + "graphQL", + "restful", + "access control", + "dashboard", + "admin panel", + "api", + "MongoDB", + "self hosted", + "react", + "auth" + ], + "license": "MIT", + "main": "./src/index.ts", + "maintainers": [ + { + "email": "info@payloadcms.com", + "name": "Payload", + "url": "https://payloadcms.com" + } + ], + "name": "payload", + "publishConfig": { + "access": "public", + "exports": { + ".": { + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./*": { + "default": "./dist/exports/*.js", + "types": "./dist/exports/*.d.ts" + }, + "./scss": { + "default": "./dist/exports/scss.scss" + } + }, + "main": "./dist/index.js", + "module": "./dist/index.js", + "registry": "https://registry.npmjs.org/", + "types": "./dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/payloadcms/payload.git" + }, + "scripts": { + "build": "pnpm copyfiles && pnpm build:tsc && pnpm build:components", + "build:components": "webpack --config dist/bundlers/webpack/components.config.js", + "build:tsc": "tsc -p tsconfig.build.json", + "build:watch": "nodemon --watch 'src/**' --ext 'ts,tsx' --exec \"pnpm build:tsc\"", + "clean": "rimraf dist", + "clean:cache": "rimraf node_modules/.cache", + "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/", + "dev": "nodemon", + "dev:generate-graphql-schema": "ts-node -T ./test/generateGraphQLSchema.ts", + "dev:generate-types": "ts-node -T ./test/generateTypes.ts", + "dev:postgres": "cross-env PAYLOAD_DATABASE=postgres nodemon", + "fix": "eslint \"src/**/*.ts\" --fix", + "lint": "eslint \"src/**/*.ts\"", + "pretest": "pnpm build", + "release:beta": "release-it pre --preReleaseId=beta --npm.tag=beta --config .release-it.pre.json", + "release:canary": "release-it pre --preReleaseId=canary --npm.tag=canary --config .release-it.pre.json", + "release:major": "release-it major", + "release:minor": "release-it minor", + "release:patch": "release-it patch", + "test": "pnpm test:int && pnpm test:components && pnpm test:e2e", + "test:components": "cross-env jest --config=jest.components.config.js", + "test:e2e": "ts-node -T ./test/runE2E.ts", + "test:e2e:debug": "cross-env PWDEBUG=1 DISABLE_LOGGING=true playwright test", + "test:e2e:headed": "cross-env DISABLE_LOGGING=true playwright test --headed", + "test:int": "cross-env DISABLE_LOGGING=true jest --forceExit --detectOpenHandles", + "translateNewKeys": "ts-node -T ./scripts/translateNewKeys.ts" + }, + "sideEffects": false, + "types": "./src/index.ts", + "version": "1.14.0" } diff --git a/packages/payload/playwright.bail.config.ts b/packages/payload/playwright.bail.config.ts index 2fd1fe943..0ea4db790 100644 --- a/packages/payload/playwright.bail.config.ts +++ b/packages/payload/playwright.bail.config.ts @@ -1,9 +1,10 @@ -import type { PlaywrightTestConfig } from '@playwright/test'; -import baseConfig from './playwright.config'; +import type { PlaywrightTestConfig } from '@playwright/test' + +import baseConfig from './playwright.config' const config: PlaywrightTestConfig = { ...baseConfig, maxFailures: 1, -}; +} -export default config; +export default config diff --git a/packages/payload/playwright.config.ts b/packages/payload/playwright.config.ts index 655d8f6c6..2aae8af58 100644 --- a/packages/payload/playwright.config.ts +++ b/packages/payload/playwright.config.ts @@ -1,15 +1,15 @@ -import type { PlaywrightTestConfig } from '@playwright/test'; +import type { PlaywrightTestConfig } from '@playwright/test' const config: PlaywrightTestConfig = { // Look for test files in the "test" directory, relative to this configuration file testDir: 'test', testMatch: '*e2e.spec.ts', - workers: 999, timeout: 180000, // 3 minutes use: { - video: 'retain-on-failure', - trace: 'retain-on-failure', screenshot: 'only-on-failure', + trace: 'retain-on-failure', + video: 'retain-on-failure', }, -}; -export default config; + workers: 999, +} +export default config diff --git a/packages/payload/schema.graphql b/packages/payload/schema.graphql index 1f1c1a031..8dc3f178e 100644 --- a/packages/payload/schema.graphql +++ b/packages/payload/schema.graphql @@ -783,7 +783,8 @@ type User { """ A field whose value conforms to the standard internet email address format as specified in HTML Spec: https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address. """ -scalar EmailAddress @specifiedBy(url: "https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address") +scalar EmailAddress + @specifiedBy(url: "https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address") type Users { docs: [User] @@ -1604,7 +1605,12 @@ type Mutation { updatePost(id: String!, data: mutationPostUpdateInput!, draft: Boolean, autosave: Boolean): Post deletePost(id: String!): Post createMedia(data: mutationMediaInput!, draft: Boolean): Media - updateMedia(id: String!, data: mutationMediaUpdateInput!, draft: Boolean, autosave: Boolean): Media + updateMedia( + id: String! + data: mutationMediaUpdateInput! + draft: Boolean + autosave: Boolean + ): Media deleteMedia(id: String!): Media createUser(data: mutationUserInput!, draft: Boolean): User updateUser(id: String!, data: mutationUserUpdateInput!, draft: Boolean, autosave: Boolean): User @@ -1703,4 +1709,4 @@ type usersResetPassword { input mutationMenuInput { globalText: String -} \ No newline at end of file +} diff --git a/packages/payload/scripts/translateNewKeys.ts b/packages/payload/scripts/translateNewKeys.ts index fd0c8f137..8f71ee345 100644 --- a/packages/payload/scripts/translateNewKeys.ts +++ b/packages/payload/scripts/translateNewKeys.ts @@ -1,72 +1,74 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable no-continue */ /* eslint-disable no-restricted-syntax */ -import * as fs from 'fs'; -import * as path from 'path'; - -const TRANSLATIONS_DIR = './src/translations'; -const SOURCE_LANG_FILE = 'en.json'; -const OPENAI_ENDPOINT = 'https://api.openai.com/v1/chat/completions'; // Adjust if needed -const OPENAI_API_KEY = 'sk-YOURKEYHERE'; // Remember to replace with your actual key +import * as fs from 'fs' +import * as path from 'path' +const TRANSLATIONS_DIR = './src/translations' +const SOURCE_LANG_FILE = 'en.json' +const OPENAI_ENDPOINT = 'https://api.openai.com/v1/chat/completions' // Adjust if needed +const OPENAI_API_KEY = 'sk-YOURKEYHERE' // Remember to replace with your actual key async function main() { - const sourceLangContent = JSON.parse(fs.readFileSync(path.join(TRANSLATIONS_DIR, SOURCE_LANG_FILE), 'utf8')); + const sourceLangContent = JSON.parse( + fs.readFileSync(path.join(TRANSLATIONS_DIR, SOURCE_LANG_FILE), 'utf8'), + ) - const files = fs.readdirSync(TRANSLATIONS_DIR); + const files = fs.readdirSync(TRANSLATIONS_DIR) for (const file of files) { if (file === SOURCE_LANG_FILE) { - continue; + continue } // check if file ends with .json if (!file.endsWith('.json')) { - continue; + continue } // skip the translation-schema.json file if (file === 'translation-schema.json') { - continue; + continue } - console.log('Processing file:', file); + console.log('Processing file:', file) - const targetLangContent = JSON.parse(fs.readFileSync(path.join(TRANSLATIONS_DIR, file), 'utf8')); - const missingKeys = findMissingKeys(sourceLangContent, targetLangContent); + const targetLangContent = JSON.parse(fs.readFileSync(path.join(TRANSLATIONS_DIR, file), 'utf8')) + const missingKeys = findMissingKeys(sourceLangContent, targetLangContent) - let hasChanged = false; + let hasChanged = false for (const missingKey of missingKeys) { - const keys = missingKey.split('.'); - const sourceText = keys.reduce((acc, key) => acc[key], sourceLangContent); - const targetLang = file.split('.')[0]; + const keys = missingKey.split('.') + const sourceText = keys.reduce((acc, key) => acc[key], sourceLangContent) + const targetLang = file.split('.')[0] - const translatedText = await translateText(sourceText, targetLang); - let targetObj = targetLangContent; + const translatedText = await translateText(sourceText, targetLang) + let targetObj = targetLangContent for (let i = 0; i < keys.length - 1; i += 1) { if (!targetObj[keys[i]]) { - targetObj[keys[i]] = {}; + targetObj[keys[i]] = {} } - targetObj = targetObj[keys[i]]; + targetObj = targetObj[keys[i]] } - targetObj[keys[keys.length - 1]] = translatedText; - hasChanged = true; + targetObj[keys[keys.length - 1]] = translatedText + hasChanged = true } - if (hasChanged) { - const sortedContent = sortKeys(targetLangContent); - fs.writeFileSync(path.join(TRANSLATIONS_DIR, file), JSON.stringify(sortedContent, null, 2)); + const sortedContent = sortKeys(targetLangContent) + fs.writeFileSync(path.join(TRANSLATIONS_DIR, file), JSON.stringify(sortedContent, null, 2)) } } } -main().then(() => { - console.log('Translation update completed.'); -}).catch((error) => { - console.error('Error occurred:', error); -}); +main() + .then(() => { + console.log('Translation update completed.') + }) + .catch((error) => { + console.error('Error occurred:', error) + }) async function translateText(text: string, targetLang: string): Promise { const response = await fetch(OPENAI_ENDPOINT, { @@ -89,40 +91,42 @@ async function translateText(text: string, targetLang: string): Promise }, ], }), - }); + }) - const data = await response.json(); - console.log(' Old text:', text, 'New text:', data.choices[0].message.content.trim()); - return data.choices[0].message.content.trim(); + const data = await response.json() + console.log(' Old text:', text, 'New text:', data.choices[0].message.content.trim()) + return data.choices[0].message.content.trim() } function findMissingKeys(baseObj: any, targetObj: any, prefix = ''): string[] { - let missingKeys = []; + let missingKeys = [] for (const key in baseObj) { if (typeof baseObj[key] === 'object') { - missingKeys = missingKeys.concat(findMissingKeys(baseObj[key], targetObj[key] || {}, `${prefix}${key}.`)); + missingKeys = missingKeys.concat( + findMissingKeys(baseObj[key], targetObj[key] || {}, `${prefix}${key}.`), + ) } else if (!(key in targetObj)) { - missingKeys.push(`${prefix}${key}`); + missingKeys.push(`${prefix}${key}`) } } - return missingKeys; + return missingKeys } function sortKeys(obj: any): any { - if (typeof obj !== 'object' || obj === null) return obj; + if (typeof obj !== 'object' || obj === null) return obj if (Array.isArray(obj)) { - return obj.map(sortKeys); + return obj.map(sortKeys) } - const sortedKeys = Object.keys(obj).sort(); - const sortedObj: { [key: string]: any } = {}; + const sortedKeys = Object.keys(obj).sort() + const sortedObj: { [key: string]: any } = {} for (const key of sortedKeys) { - sortedObj[key] = sortKeys(obj[key]); + sortedObj[key] = sortKeys(obj[key]) } - return sortedObj; + return sortedObj } diff --git a/packages/payload/src/admin/Root.tsx b/packages/payload/src/admin/Root.tsx index 6b0bbe569..fb657ba21 100644 --- a/packages/payload/src/admin/Root.tsx +++ b/packages/payload/src/admin/Root.tsx @@ -1,46 +1,43 @@ -'use client'; +'use client' // eslint-disable-next-line @typescript-eslint/ban-ts-comment +import { ModalContainer, ModalProvider } from '@faceless-ui/modal' +import { ScrollInfoProvider } from '@faceless-ui/scroll-info' +import { WindowInfoProvider } from '@faceless-ui/window-info' // @ts-ignore - need to do this because this file doesn't actually exist -import config from 'payload-config'; -import React from 'react'; -import { BrowserRouter as Router } from 'react-router-dom'; -import { ScrollInfoProvider } from '@faceless-ui/scroll-info'; -import { WindowInfoProvider } from '@faceless-ui/window-info'; -import { ModalProvider, ModalContainer } from '@faceless-ui/modal'; -import { ToastContainer, Slide } from 'react-toastify'; -import { AuthProvider } from './components/utilities/Auth'; -import { ConfigProvider } from './components/utilities/Config'; -import { PreferencesProvider } from './components/utilities/Preferences'; -import { CustomProvider } from './components/utilities/CustomProvider'; -import { SearchParamsProvider } from './components/utilities/SearchParams'; -import { LocaleProvider } from './components/utilities/Locale'; -import Routes from './components/Routes'; -import { StepNavProvider } from './components/elements/StepNav'; -import { ThemeProvider } from './components/utilities/Theme'; -import { I18n } from './components/utilities/I18n'; -import { LoadingOverlayProvider } from './components/utilities/LoadingOverlay'; +import config from 'payload-config' +import React from 'react' +import { BrowserRouter as Router } from 'react-router-dom' +import { Slide, ToastContainer } from 'react-toastify' -import './scss/app.scss'; +import Routes from './components/Routes' +import { StepNavProvider } from './components/elements/StepNav' +import { AuthProvider } from './components/utilities/Auth' +import { ConfigProvider } from './components/utilities/Config' +import { CustomProvider } from './components/utilities/CustomProvider' +import { I18n } from './components/utilities/I18n' +import { LoadingOverlayProvider } from './components/utilities/LoadingOverlay' +import { LocaleProvider } from './components/utilities/Locale' +import { PreferencesProvider } from './components/utilities/Preferences' +import { SearchParamsProvider } from './components/utilities/SearchParams' +import { ThemeProvider } from './components/utilities/Theme' +import './scss/app.scss' const Root = () => ( - - + @@ -64,12 +61,8 @@ const Root = () => ( - + -); +) -export default Root; +export default Root diff --git a/packages/payload/src/admin/api.ts b/packages/payload/src/admin/api.ts index 3c6b3ff2e..a0e7eca4a 100644 --- a/packages/payload/src/admin/api.ts +++ b/packages/payload/src/admin/api.ts @@ -1,78 +1,78 @@ -import qs from 'qs'; +import qs from 'qs' type GetOptions = RequestInit & { params?: Record } export const requests = { + delete: (url: string, options: RequestInit = { headers: {} }): Promise => { + const headers = options && options.headers ? { ...options.headers } : {} + + const formattedOptions: RequestInit = { + ...options, + credentials: 'include', + headers: { + ...headers, + }, + method: 'delete', + } + + return fetch(url, formattedOptions) + }, + get: (url: string, options: GetOptions = { headers: {} }): Promise => { - let query = ''; + let query = '' if (options.params) { - query = qs.stringify(options.params, { addQueryPrefix: true }); + query = qs.stringify(options.params, { addQueryPrefix: true }) } return fetch(`${url}${query}`, { credentials: 'include', ...options, - }); - }, - - post: (url: string, options: RequestInit = { headers: {} }): Promise => { - const headers = options && options.headers ? { ...options.headers } : {}; - - const formattedOptions: RequestInit = { - ...options, - method: 'post', - credentials: 'include', - headers: { - ...headers, - }, - }; - - return fetch(`${url}`, formattedOptions); - }, - - put: (url: string, options: RequestInit = { headers: {} }): Promise => { - const headers = options && options.headers ? { ...options.headers } : {}; - - const formattedOptions: RequestInit = { - ...options, - method: 'put', - credentials: 'include', - headers: { - ...headers, - }, - }; - - return fetch(url, formattedOptions); + }) }, patch: (url: string, options: RequestInit = { headers: {} }): Promise => { - const headers = options && options.headers ? { ...options.headers } : {}; + const headers = options && options.headers ? { ...options.headers } : {} const formattedOptions: RequestInit = { ...options, + credentials: 'include', + headers: { + ...headers, + }, method: 'PATCH', - credentials: 'include', - headers: { - ...headers, - }, - }; + } - return fetch(url, formattedOptions); + return fetch(url, formattedOptions) }, - delete: (url: string, options: RequestInit = { headers: {} }): Promise => { - const headers = options && options.headers ? { ...options.headers } : {}; + post: (url: string, options: RequestInit = { headers: {} }): Promise => { + const headers = options && options.headers ? { ...options.headers } : {} const formattedOptions: RequestInit = { ...options, - method: 'delete', credentials: 'include', headers: { ...headers, }, - }; + method: 'post', + } - return fetch(url, formattedOptions); + return fetch(`${url}`, formattedOptions) }, -}; + + put: (url: string, options: RequestInit = { headers: {} }): Promise => { + const headers = options && options.headers ? { ...options.headers } : {} + + const formattedOptions: RequestInit = { + ...options, + credentials: 'include', + headers: { + ...headers, + }, + method: 'put', + } + + return fetch(url, formattedOptions) + }, +} diff --git a/packages/payload/src/admin/assets/assets.d.ts b/packages/payload/src/admin/assets/assets.d.ts index 67591d000..7cca990e9 100644 --- a/packages/payload/src/admin/assets/assets.d.ts +++ b/packages/payload/src/admin/assets/assets.d.ts @@ -1,22 +1,22 @@ declare module '*.svg' { - import React = require('react'); + import React = require('react') - export const ReactComponent: React.SFC>; - const src: string; - export default src; + export const ReactComponent: React.SFC> + const src: string + export default src } declare module '*.jpg' { - const content: string; - export default content; + const content: string + export default content } declare module '*.png' { - const content: string; - export default content; + const content: string + export default content } declare module '*.json' { - const content: string; - export default content; + const content: string + export default content } diff --git a/packages/payload/src/admin/components/Routes.tsx b/packages/payload/src/admin/components/Routes.tsx index 96a548a2c..a30d69f08 100644 --- a/packages/payload/src/admin/components/Routes.tsx +++ b/packages/payload/src/admin/components/Routes.tsx @@ -1,90 +1,85 @@ -import React, { Fragment, lazy, Suspense, useEffect, useState } from 'react'; -import { Redirect, Route, Switch } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import { useAuth } from './utilities/Auth'; -import { useConfig } from './utilities/Config'; -import List from './views/collections/List'; -import DefaultTemplate from './templates/Default'; -import { requests } from '../api'; -import StayLoggedIn from './modals/StayLoggedIn'; -import Versions from './views/Versions'; -import Version from './views/Version'; -import { DocumentInfoProvider } from './utilities/DocumentInfo'; -import { useLocale } from './utilities/Locale'; -import { LoadingOverlayToggle } from './elements/Loading'; +import React, { Fragment, Suspense, lazy, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Redirect, Route, Switch } from 'react-router-dom' -const Dashboard = lazy(() => import('./views/Dashboard')); -const ForgotPassword = lazy(() => import('./views/ForgotPassword')); -const Login = lazy(() => import('./views/Login')); -const Logout = lazy(() => import('./views/Logout')); -const NotFound = lazy(() => import('./views/NotFound')); -const Verify = lazy(() => import('./views/Verify')); -const CreateFirstUser = lazy(() => import('./views/CreateFirstUser')); -const Edit = lazy(() => import('./views/collections/Edit')); -const EditGlobal = lazy(() => import('./views/Global')); -const ResetPassword = lazy(() => import('./views/ResetPassword')); -const Unauthorized = lazy(() => import('./views/Unauthorized')); -const Account = lazy(() => import('./views/Account')); +import { requests } from '../api' +import { LoadingOverlayToggle } from './elements/Loading' +import StayLoggedIn from './modals/StayLoggedIn' +import DefaultTemplate from './templates/Default' +import { useAuth } from './utilities/Auth' +import { useConfig } from './utilities/Config' +import { DocumentInfoProvider } from './utilities/DocumentInfo' +import { useLocale } from './utilities/Locale' +import Version from './views/Version' +import Versions from './views/Versions' +import List from './views/collections/List' + +const Dashboard = lazy(() => import('./views/Dashboard')) +const ForgotPassword = lazy(() => import('./views/ForgotPassword')) +const Login = lazy(() => import('./views/Login')) +const Logout = lazy(() => import('./views/Logout')) +const NotFound = lazy(() => import('./views/NotFound')) +const Verify = lazy(() => import('./views/Verify')) +const CreateFirstUser = lazy(() => import('./views/CreateFirstUser')) +const Edit = lazy(() => import('./views/collections/Edit')) +const EditGlobal = lazy(() => import('./views/Global')) +const ResetPassword = lazy(() => import('./views/ResetPassword')) +const Unauthorized = lazy(() => import('./views/Unauthorized')) +const Account = lazy(() => import('./views/Account')) const Routes: React.FC = () => { - const [initialized, setInitialized] = useState(null); - const { user, permissions, refreshCookie } = useAuth(); - const { i18n } = useTranslation(); - const { code: locale } = useLocale(); + const [initialized, setInitialized] = useState(null) + const { permissions, refreshCookie, user } = useAuth() + const { i18n } = useTranslation() + const { code: locale } = useLocale() - const canAccessAdmin = permissions?.canAccessAdmin; + const canAccessAdmin = permissions?.canAccessAdmin - const config = useConfig(); + const config = useConfig() const { admin: { - user: userSlug, - logoutRoute, + components: { routes: customRoutes } = {}, inactivityRoute: logoutInactivityRoute, - components: { - routes: customRoutes, - } = {}, + logoutRoute, + user: userSlug, }, - routes, collections, globals, - } = config; + routes, + } = config - const isLoadingUser = Boolean(typeof user === 'undefined' || (user && typeof canAccessAdmin === 'undefined')); - const userCollection = collections.find(({ slug }) => slug === userSlug); + const isLoadingUser = Boolean( + typeof user === 'undefined' || (user && typeof canAccessAdmin === 'undefined'), + ) + const userCollection = collections.find(({ slug }) => slug === userSlug) useEffect(() => { - const { slug } = userCollection; + const { slug } = userCollection if (!userCollection.auth.disableLocalStrategy) { - requests.get(`${routes.api}/${slug}/init`, { - headers: { - 'Accept-Language': i18n.language, - }, - }).then((res) => res.json().then((data) => { - if (data && 'initialized' in data) { - setInitialized(data.initialized); - } - })); + requests + .get(`${routes.api}/${slug}/init`, { + headers: { + 'Accept-Language': i18n.language, + }, + }) + .then((res) => + res.json().then((data) => { + if (data && 'initialized' in data) { + setInitialized(data.initialized) + } + }), + ) } else { - setInitialized(true); + setInitialized(true) } - }, [i18n.language, routes, userCollection]); + }, [i18n.language, routes, userCollection]) return ( - - )} - > - + }> + { if (initialized === false) { return ( @@ -96,26 +91,24 @@ const Routes: React.FC = () => { - ); + ) } if (initialized === true && !isLoadingUser) { return ( - {Array.isArray(customRoutes) && customRoutes.map(({ path, Component, strict, exact, sensitive }) => ( - - - - ))} + {Array.isArray(customRoutes) && + customRoutes.map(({ Component, exact, path, sensitive, strict }) => ( + + + + ))} @@ -141,15 +134,15 @@ const Routes: React.FC = () => { if (collection?.auth?.verify && !collection.auth.disableLocalStrategy) { return ( - ); + ) } - return null; + return null })} @@ -158,10 +151,7 @@ const Routes: React.FC = () => { {canAccessAdmin && ( - + @@ -173,29 +163,33 @@ const Routes: React.FC = () => { {collections - .filter(({ admin: { hidden } }) => !(typeof hidden === 'function' ? hidden({ user }) : hidden)) + .filter( + ({ admin: { hidden } }) => + !(typeof hidden === 'function' ? hidden({ user }) : hidden), + ) .reduce((collectionRoutes, collection) => { const routesToReturn = [ ...collectionRoutes, - {permissions?.collections?.[collection.slug]?.read?.permission - ? - : } + {permissions?.collections?.[collection.slug]?.read + ?.permission ? ( + + ) : ( + + )} , - {permissions?.collections?.[collection.slug]?.create?.permission ? ( - + {permissions?.collections?.[collection.slug]?.create + ?.permission ? ( + ) : ( @@ -203,107 +197,119 @@ const Routes: React.FC = () => { )} , - {permissions?.collections?.[collection.slug]?.read?.permission ? ( - - + {permissions?.collections?.[collection.slug]?.read + ?.permission ? ( + + - ) : } + ) : ( + + )} , - ]; + ] if (collection.versions) { routesToReturn.push( - {permissions?.collections?.[collection.slug]?.readVersions?.permission ? ( + {permissions?.collections?.[collection.slug]?.readVersions + ?.permission ? ( - ) : } + ) : ( + + )} , - ); + ) routesToReturn.push( - {permissions?.collections?.[collection.slug]?.readVersions?.permission ? ( - + {permissions?.collections?.[collection.slug]?.readVersions + ?.permission ? ( + - ) : } + ) : ( + + )} , - ); + ) } - return routesToReturn; + return routesToReturn }, [])} - {globals && globals - .filter(({ admin: { hidden } }) => !(typeof hidden === 'function' ? hidden({ user }) : hidden)) - .reduce((globalRoutes, global) => { - const routesToReturn = [ - ...globalRoutes, - - {permissions?.globals?.[global.slug]?.read?.permission ? ( - - ) : } - , - ]; + {permissions?.globals?.[global.slug]?.readVersions + ?.permission ? ( + + ) : ( + + )} + , + ) - if (global.versions) { - routesToReturn.push( - - {permissions?.globals?.[global.slug]?.readVersions?.permission - ? - : } - , - ); + routesToReturn.push( + + {permissions?.globals?.[global.slug]?.readVersions + ?.permission ? ( + + ) : ( + + )} + , + ) + } - routesToReturn.push( - - {permissions?.globals?.[global.slug]?.readVersions?.permission ? ( - - ) : } - , - ); - } - - return routesToReturn; - }, [])} + return routesToReturn + }, [])} @@ -311,25 +317,34 @@ const Routes: React.FC = () => { )} - {canAccessAdmin === false && ( - - )} + {canAccessAdmin === false && } - ) : } + ) : ( + + )} - ); + ) } - return null; + return null }} + path={routes.admin} /> - ); -}; + ) +} -export default Routes; +export default Routes diff --git a/packages/payload/src/admin/components/elements/ArrayAction/index.scss b/packages/payload/src/admin/components/elements/ArrayAction/index.scss index c7682aced..ef539d654 100644 --- a/packages/payload/src/admin/components/elements/ArrayAction/index.scss +++ b/packages/payload/src/admin/components/elements/ArrayAction/index.scss @@ -34,7 +34,7 @@ svg { position: relative; top: -1px; - margin-right: base(.25); + margin-right: base(0.25); .stroke { stroke-width: 1px; @@ -42,7 +42,7 @@ } &:hover { - opacity: .7; + opacity: 0.7; } } diff --git a/packages/payload/src/admin/components/elements/ArrayAction/index.tsx b/packages/payload/src/admin/components/elements/ArrayAction/index.tsx index 43e4bc8b9..c017c9ccb 100644 --- a/packages/payload/src/admin/components/elements/ArrayAction/index.tsx +++ b/packages/payload/src/admin/components/elements/ArrayAction/index.tsx @@ -1,44 +1,41 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import Popup from '../Popup'; -import More from '../../icons/More'; -import Chevron from '../../icons/Chevron'; -import { Props } from './types'; -import Plus from '../../icons/Plus'; -import X from '../../icons/X'; -import Copy from '../../icons/Copy'; +import React from 'react' +import { useTranslation } from 'react-i18next' -import './index.scss'; +import type { Props } from './types' -const baseClass = 'array-actions'; +import Chevron from '../../icons/Chevron' +import Copy from '../../icons/Copy' +import More from '../../icons/More' +import Plus from '../../icons/Plus' +import X from '../../icons/X' +import Popup from '../Popup' +import './index.scss' + +const baseClass = 'array-actions' export const ArrayAction: React.FC = ({ - moveRow, - index, - rowCount, addRow, duplicateRow, - removeRow, hasMaxRows, + index, + moveRow, + removeRow, + rowCount, }) => { - const { t } = useTranslation('general'); + const { t } = useTranslation('general') return ( } render={({ close }) => { return ( {index !== 0 && ( - ); + ) }} + button={} + buttonClassName={`${baseClass}__button`} + className={baseClass} + horizontalAlign="center" /> - ); -}; + ) +} diff --git a/packages/payload/src/admin/components/elements/ArrayAction/types.ts b/packages/payload/src/admin/components/elements/ArrayAction/types.ts index 337066aab..5e2addaca 100644 --- a/packages/payload/src/admin/components/elements/ArrayAction/types.ts +++ b/packages/payload/src/admin/components/elements/ArrayAction/types.ts @@ -1,9 +1,9 @@ export type Props = { addRow: (current: number, blockType?: string) => void duplicateRow: (current: number) => void - removeRow: (index: number) => void - moveRow: (from: number, to: number) => void - index: number - rowCount: number hasMaxRows: boolean + index: number + moveRow: (from: number, to: number) => void + removeRow: (index: number) => void + rowCount: number } diff --git a/packages/payload/src/admin/components/elements/Autosave/index.tsx b/packages/payload/src/admin/components/elements/Autosave/index.tsx index 386218720..28940b7ff 100644 --- a/packages/payload/src/admin/components/elements/Autosave/index.tsx +++ b/packages/payload/src/admin/components/elements/Autosave/index.tsx @@ -1,98 +1,107 @@ -import { useHistory } from 'react-router-dom'; -import { toast } from 'react-toastify'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useConfig } from '../../utilities/Config'; -import { useFormModified, useAllFormFields } from '../../forms/Form/context'; -import { useLocale } from '../../utilities/Locale'; -import { Props } from './types'; -import reduceFieldsToValues from '../../forms/Form/reduceFieldsToValues'; -import { useDocumentInfo } from '../../utilities/DocumentInfo'; -import useDebounce from '../../../hooks/useDebounce'; +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useHistory } from 'react-router-dom' +import { toast } from 'react-toastify' -import './index.scss'; +import type { Props } from './types' -const baseClass = 'autosave'; +import useDebounce from '../../../hooks/useDebounce' +import { useAllFormFields, useFormModified } from '../../forms/Form/context' +import reduceFieldsToValues from '../../forms/Form/reduceFieldsToValues' +import { useConfig } from '../../utilities/Config' +import { useDocumentInfo } from '../../utilities/DocumentInfo' +import { useLocale } from '../../utilities/Locale' +import './index.scss' + +const baseClass = 'autosave' const Autosave: React.FC = ({ collection, global, id, publishedDocUpdatedAt }) => { - const { serverURL, routes: { api, admin } } = useConfig(); - const { versions, getVersions } = useDocumentInfo(); - const [fields] = useAllFormFields(); - const modified = useFormModified(); - const { code: locale } = useLocale(); - const { replace } = useHistory(); - const { t, i18n } = useTranslation('version'); + const { + routes: { admin, api }, + serverURL, + } = useConfig() + const { getVersions, versions } = useDocumentInfo() + const [fields] = useAllFormFields() + const modified = useFormModified() + const { code: locale } = useLocale() + const { replace } = useHistory() + const { i18n, t } = useTranslation('version') - let interval = 800; - if (collection?.versions.drafts && collection.versions?.drafts?.autosave) interval = collection.versions.drafts.autosave.interval; - if (global?.versions.drafts && global.versions?.drafts?.autosave) interval = global.versions.drafts.autosave.interval; + let interval = 800 + if (collection?.versions.drafts && collection.versions?.drafts?.autosave) + interval = collection.versions.drafts.autosave.interval + if (global?.versions.drafts && global.versions?.drafts?.autosave) + interval = global.versions.drafts.autosave.interval - const [saving, setSaving] = useState(false); - const [lastSaved, setLastSaved] = useState(); - const debouncedFields = useDebounce(fields, interval); - const fieldRef = useRef(fields); - const modifiedRef = useRef(modified); - const localeRef = useRef(locale); + const [saving, setSaving] = useState(false) + const [lastSaved, setLastSaved] = useState() + const debouncedFields = useDebounce(fields, interval) + const fieldRef = useRef(fields) + const modifiedRef = useRef(modified) + const localeRef = useRef(locale) // Store fields in ref so the autosave func // can always retrieve the most to date copies // after the timeout has executed - fieldRef.current = fields; + fieldRef.current = fields // Store modified in ref so the autosave func // can bail out if modified becomes false while // timing out during autosave - modifiedRef.current = modified; + modifiedRef.current = modified const createCollectionDoc = useCallback(async () => { - const res = await fetch(`${serverURL}${api}/${collection.slug}?locale=${locale}&fallback-locale=null&depth=0&draft=true&autosave=true`, { - method: 'POST', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - 'Accept-Language': i18n.language, + const res = await fetch( + `${serverURL}${api}/${collection.slug}?locale=${locale}&fallback-locale=null&depth=0&draft=true&autosave=true`, + { + body: JSON.stringify({}), + credentials: 'include', + headers: { + 'Accept-Language': i18n.language, + 'Content-Type': 'application/json', + }, + method: 'POST', }, - body: JSON.stringify({}), - }); + ) if (res.status === 201) { - const json = await res.json(); + const json = await res.json() replace(`${admin}/collections/${collection.slug}/${json.doc.id}`, { state: { data: json.doc, }, - }); + }) } else { - toast.error(t('error:autosaving')); + toast.error(t('error:autosaving')) } - }, [i18n, serverURL, api, collection, locale, replace, admin, t]); + }, [i18n, serverURL, api, collection, locale, replace, admin, t]) useEffect(() => { // If no ID, but this is used for a collection doc, // Immediately save it and set lastSaved if (!id && collection) { - createCollectionDoc(); + createCollectionDoc() } - }, [id, collection, createCollectionDoc]); + }, [id, collection, createCollectionDoc]) // When debounced fields change, autosave useEffect(() => { const autosave = async () => { if (modified) { - setSaving(true); + setSaving(true) - let url: string; - let method: string; + let url: string + let method: string if (collection && id) { - url = `${serverURL}${api}/${collection.slug}/${id}?draft=true&autosave=true&locale=${localeRef.current}`; - method = 'PATCH'; + url = `${serverURL}${api}/${collection.slug}/${id}?draft=true&autosave=true&locale=${localeRef.current}` + method = 'PATCH' } if (global) { - url = `${serverURL}${api}/globals/${global.slug}?draft=true&autosave=true&locale=${localeRef.current}`; - method = 'POST'; + url = `${serverURL}${api}/globals/${global.slug}?draft=true&autosave=true&locale=${localeRef.current}` + method = 'POST' } if (url) { @@ -101,45 +110,57 @@ const Autosave: React.FC = ({ collection, global, id, publishedDocUpdated const body = { ...reduceFieldsToValues(fieldRef.current, true), _status: 'draft', - }; + } const res = await fetch(url, { - method, + body: JSON.stringify(body), credentials: 'include', headers: { - 'Content-Type': 'application/json', 'Accept-Language': i18n.language, + 'Content-Type': 'application/json', }, - body: JSON.stringify(body), - }); + method, + }) if (res.status === 200) { - setLastSaved(new Date().getTime()); - getVersions(); + setLastSaved(new Date().getTime()) + getVersions() } } - setSaving(false); - }, 1000); + setSaving(false) + }, 1000) } } - }; + } - autosave(); - }, [i18n, debouncedFields, modified, serverURL, api, collection, global, id, getVersions, localeRef, modifiedRef]); + autosave() + }, [ + i18n, + debouncedFields, + modified, + serverURL, + api, + collection, + global, + id, + getVersions, + localeRef, + modifiedRef, + ]) useEffect(() => { if (versions?.docs?.[0]) { - setLastSaved(new Date(versions.docs[0].updatedAt).getTime()); + setLastSaved(new Date(versions.docs[0].updatedAt).getTime()) } else if (publishedDocUpdatedAt) { - setLastSaved(new Date(publishedDocUpdatedAt).getTime()); + setLastSaved(new Date(publishedDocUpdatedAt).getTime()) } - }, [publishedDocUpdatedAt, versions]); + }, [publishedDocUpdatedAt, versions]) return (
{saving && t('saving')} - {(!saving && lastSaved) && ( + {!saving && lastSaved && ( {t('lastSavedAgo', { distance: Math.round((Number(new Date(lastSaved)) - Number(new Date())) / 1000 / 60), @@ -147,7 +168,7 @@ const Autosave: React.FC = ({ collection, global, id, publishedDocUpdated )}
- ); -}; + ) +} -export default Autosave; +export default Autosave diff --git a/packages/payload/src/admin/components/elements/Autosave/types.ts b/packages/payload/src/admin/components/elements/Autosave/types.ts index 9591fc542..fe7d05b4a 100644 --- a/packages/payload/src/admin/components/elements/Autosave/types.ts +++ b/packages/payload/src/admin/components/elements/Autosave/types.ts @@ -1,9 +1,9 @@ -import { SanitizedCollectionConfig } from '../../../../collections/config/types'; -import { SanitizedGlobalConfig } from '../../../../globals/config/types'; +import type { SanitizedCollectionConfig } from '../../../../collections/config/types' +import type { SanitizedGlobalConfig } from '../../../../globals/config/types' export type Props = { - collection?: SanitizedCollectionConfig, - global?: SanitizedGlobalConfig, - id?: string | number + collection?: SanitizedCollectionConfig + global?: SanitizedGlobalConfig + id?: number | string publishedDocUpdatedAt: string } diff --git a/packages/payload/src/admin/components/elements/Banner/index.scss b/packages/payload/src/admin/components/elements/Banner/index.scss index 7c5579b04..5875595ff 100644 --- a/packages/payload/src/admin/components/elements/Banner/index.scss +++ b/packages/payload/src/admin/components/elements/Banner/index.scss @@ -8,7 +8,7 @@ background: var(--theme-elevation-100); color: var(--theme-elevation-800); border-radius: $style-radius-s; - padding: base(.5); + padding: base(0.5); margin-bottom: $baseline; &--has-action { diff --git a/packages/payload/src/admin/components/elements/Banner/index.tsx b/packages/payload/src/admin/components/elements/Banner/index.tsx index f7b69d07b..412dab222 100644 --- a/packages/payload/src/admin/components/elements/Banner/index.tsx +++ b/packages/payload/src/admin/components/elements/Banner/index.tsx @@ -1,18 +1,19 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; -import { Props, RenderedTypeProps } from './types'; +import React from 'react' +import { Link } from 'react-router-dom' -import './index.scss'; +import type { Props, RenderedTypeProps } from './types' -const baseClass = 'banner'; +import './index.scss' + +const baseClass = 'banner' export const Banner: React.FC = ({ + alignIcon = 'right', children, className, - to, icon, - alignIcon = 'right', onClick, + to, type = 'default', }) => { const classes = [ @@ -23,34 +24,22 @@ export const Banner: React.FC = ({ (to || onClick) && `${baseClass}--has-action`, icon && `${baseClass}--has-icon`, icon && `${baseClass}--align-icon-${alignIcon}`, - ].filter(Boolean).join(' '); + ] + .filter(Boolean) + .join(' ') - let RenderedType: string | React.ComponentType = 'div'; + let RenderedType: React.ComponentType | string = 'div' - if (onClick && !to) RenderedType = 'button'; - if (to) RenderedType = Link; + if (onClick && !to) RenderedType = 'button' + if (to) RenderedType = Link return ( - - {(icon && alignIcon === 'left') && ( - - {icon} - - )} - - {children} - - {(icon && alignIcon === 'right') && ( - - {icon} - - )} + + {icon && alignIcon === 'left' && {icon}} + {children} + {icon && alignIcon === 'right' && {icon}} - ); -}; + ) +} -export default Banner; +export default Banner diff --git a/packages/payload/src/admin/components/elements/Banner/types.ts b/packages/payload/src/admin/components/elements/Banner/types.ts index c1c982f84..0939a5138 100644 --- a/packages/payload/src/admin/components/elements/Banner/types.ts +++ b/packages/payload/src/admin/components/elements/Banner/types.ts @@ -1,20 +1,20 @@ -import { MouseEvent } from 'react'; +import type { MouseEvent } from 'react' type onClick = (event: MouseEvent) => void export type Props = { - children?: React.ReactNode, - className?: string, - icon?: React.ReactNode, - alignIcon?: 'left' | 'right', + alignIcon?: 'left' | 'right' + children?: React.ReactNode + className?: string + icon?: React.ReactNode onClick?: onClick - to?: string, - type?: 'error' | 'success' | 'info' | 'default', + to?: string + type?: 'default' | 'error' | 'info' | 'success' } export type RenderedTypeProps = { + children?: React.ReactNode className?: string onClick?: onClick to: string - children?: React.ReactNode } diff --git a/packages/payload/src/admin/components/elements/Button/index.scss b/packages/payload/src/admin/components/elements/Button/index.scss index 9c9bd60d9..bcf2b92e1 100644 --- a/packages/payload/src/admin/components/elements/Button/index.scss +++ b/packages/payload/src/admin/components/elements/Button/index.scss @@ -50,11 +50,11 @@ a.btn { } &--size-medium { - padding: base(.5) $baseline; + padding: base(0.5) $baseline; } &--size-small { - padding: base(.25) base(.5); + padding: base(0.25) base(0.5); } &--style-primary { @@ -66,7 +66,8 @@ a.btn { } &:not(.btn--disabled) { - &:hover, &:focus-visible { + &:hover, + &:focus-visible { background: var(--theme-elevation-750); } @@ -79,7 +80,6 @@ a.btn { box-shadow: $focus-box-shadow; outline: none; } - } &--style-secondary { @@ -91,7 +91,8 @@ a.btn { background: none; backdrop-filter: blur(5px); - &:hover, &:focus-visible { + &:hover, + &:focus-visible { background: var(--theme-elevation-100); box-shadow: $hover-box-shadow; } @@ -118,21 +119,21 @@ a.btn { border-radius: 0; &:focus { - opacity: .8; + opacity: 0.8; } &:active { - opacity: .7; + opacity: 0.7; } } &--round { border-radius: 100%; } - - [dir=rtl] &--icon { - span{ - margin-left: 5px; + + [dir='rtl'] &--icon { + span { + margin-left: 5px; } } @@ -164,13 +165,13 @@ a.btn { } .btn__icon { - margin-right: base(.5); + margin-right: base(0.5); } } &--icon-position-right { .btn__icon { - margin-left: base(.5); + margin-left: base(0.5); } } @@ -185,7 +186,8 @@ a.btn { cursor: default; } - &:hover, &:focus-visible { + &:hover, + &:focus-visible { .btn__icon { @include color-svg(var(--theme-elevation-0)); background: var(--theme-elevation-800); diff --git a/packages/payload/src/admin/components/elements/Button/index.tsx b/packages/payload/src/admin/components/elements/Button/index.tsx index 23f62a89f..2256d4843 100644 --- a/packages/payload/src/admin/components/elements/Button/index.tsx +++ b/packages/payload/src/admin/components/elements/Button/index.tsx @@ -1,47 +1,40 @@ -import React, { forwardRef, Fragment, isValidElement } from 'react'; -import { Link } from 'react-router-dom'; -import { Props } from './types'; +import React, { Fragment, forwardRef, isValidElement } from 'react' +import { Link } from 'react-router-dom' -import plus from '../../icons/Plus'; -import x from '../../icons/X'; -import chevron from '../../icons/Chevron'; -import edit from '../../icons/Edit'; -import swap from '../../icons/Swap'; -import linkIcon from '../../icons/Link'; -import Tooltip from '../Tooltip'; +import type { Props } from './types' -import './index.scss'; +import chevron from '../../icons/Chevron' +import edit from '../../icons/Edit' +import linkIcon from '../../icons/Link' +import plus from '../../icons/Plus' +import swap from '../../icons/Swap' +import x from '../../icons/X' +import Tooltip from '../Tooltip' +import './index.scss' const icons = { - plus, - x, chevron, edit, - swap, link: linkIcon, -}; + plus, + swap, + x, +} -const baseClass = 'btn'; +const baseClass = 'btn' -const ButtonContents = ({ children, icon, tooltip, showTooltip }) => { - const BuiltInIcon = icons[icon]; +const ButtonContents = ({ children, icon, showTooltip, tooltip }) => { + const BuiltInIcon = icons[icon] return ( {tooltip && ( - + {tooltip} )} - {children && ( - - {children} - - )} + {children && {children}} {icon && ( {isValidElement(icon) && icon} @@ -50,32 +43,32 @@ const ButtonContents = ({ children, icon, tooltip, showTooltip }) => { )} - ); -}; + ) +} -const Button = forwardRef((props, ref) => { +const Button = forwardRef((props, ref) => { const { - className, - id, - type = 'button', - el = 'button', - to, - url, - children, - onClick, - disabled, - icon, - iconStyle = 'without-border', + 'aria-label': ariaLabel, buttonStyle = 'primary', + children, + className, + disabled, + el = 'button', + icon, + iconPosition = 'right', + iconStyle = 'without-border', + id, + newTab, + onClick, round, size = 'medium', - iconPosition = 'right', - newTab, + to, tooltip, - 'aria-label': ariaLabel, - } = props; + type = 'button', + url, + } = props - const [showTooltip, setShowTooltip] = React.useState(false); + const [showTooltip, setShowTooltip] = React.useState(false) const classes = [ baseClass, @@ -83,87 +76,66 @@ const Button = forwardRef((props, buttonStyle && `${baseClass}--style-${buttonStyle}`, icon && `${baseClass}--icon`, iconStyle && `${baseClass}--icon-style-${iconStyle}`, - (icon && !children) && `${baseClass}--icon-only`, + icon && !children && `${baseClass}--icon-only`, disabled && `${baseClass}--disabled`, round && `${baseClass}--round`, size && `${baseClass}--size-${size}`, iconPosition && `${baseClass}--icon-position-${iconPosition}`, tooltip && `${baseClass}--has-tooltip`, - ].filter(Boolean).join(' '); + ] + .filter(Boolean) + .join(' ') function handleClick(event) { - setShowTooltip(false); - if (type !== 'submit' && onClick) event.preventDefault(); - if (onClick) onClick(event); + setShowTooltip(false) + if (type !== 'submit' && onClick) event.preventDefault() + if (onClick) onClick(event) } const buttonProps = { - id, - type, - className: classes, - disabled, 'aria-disabled': disabled, 'aria-label': ariaLabel, + className: classes, + disabled, + id, + onClick: !disabled ? handleClick : undefined, onMouseEnter: tooltip ? () => setShowTooltip(true) : undefined, onMouseLeave: tooltip ? () => setShowTooltip(false) : undefined, - onClick: !disabled ? handleClick : undefined, rel: newTab ? 'noopener noreferrer' : undefined, target: newTab ? '_blank' : undefined, - }; + type, + } switch (el) { case 'link': return ( - - + + {children} - ); + ) case 'anchor': return ( - } - href={url} - > - + }> + {children} - ); + ) default: - const Tag = el; // eslint-disable-line no-case-declarations + const Tag = el // eslint-disable-line no-case-declarations return ( - - + + {children} - ); + ) } -}); +}) -export default Button; +export default Button diff --git a/packages/payload/src/admin/components/elements/Button/types.ts b/packages/payload/src/admin/components/elements/Button/types.ts index 600b9f9c5..f7534005f 100644 --- a/packages/payload/src/admin/components/elements/Button/types.ts +++ b/packages/payload/src/admin/components/elements/Button/types.ts @@ -1,23 +1,24 @@ -import React, { ElementType, MouseEvent } from 'react'; +import type { ElementType, MouseEvent } from 'react' +import type React from 'react' export type Props = { - className?: string, - id?: string, - type?: 'submit' | 'button', - el?: 'link' | 'anchor' | ElementType, - to?: string, - url?: string, - children?: React.ReactNode, - onClick?: (event: MouseEvent) => void, - disabled?: boolean, - icon?: React.ReactNode | ['chevron' | 'x' | 'plus' | 'edit'], - iconStyle?: 'with-border' | 'without-border' | 'none', - buttonStyle?: 'primary' | 'secondary' | 'transparent' | 'error' | 'none' | 'icon-label', - buttonId?: string, - round?: boolean, - size?: 'small' | 'medium', - iconPosition?: 'left' | 'right', - newTab?: boolean - tooltip?: string 'aria-label'?: string + buttonId?: string + buttonStyle?: 'error' | 'icon-label' | 'none' | 'primary' | 'secondary' | 'transparent' + children?: React.ReactNode + className?: string + disabled?: boolean + el?: 'anchor' | 'link' | ElementType + icon?: ['chevron' | 'edit' | 'plus' | 'x'] | React.ReactNode + iconPosition?: 'left' | 'right' + iconStyle?: 'none' | 'with-border' | 'without-border' + id?: string + newTab?: boolean + onClick?: (event: MouseEvent) => void + round?: boolean + size?: 'medium' | 'small' + to?: string + tooltip?: string + type?: 'button' | 'submit' + url?: string } diff --git a/packages/payload/src/admin/components/elements/Card/index.scss b/packages/payload/src/admin/components/elements/Card/index.scss index 1380c5bc5..ad43d1c10 100644 --- a/packages/payload/src/admin/components/elements/Card/index.scss +++ b/packages/payload/src/admin/components/elements/Card/index.scss @@ -15,7 +15,7 @@ &__actions { position: relative; z-index: 2; - margin-top: base(.5); + margin-top: base(0.5); display: inline-flex; .btn { diff --git a/packages/payload/src/admin/components/elements/Card/index.tsx b/packages/payload/src/admin/components/elements/Card/index.tsx index 65050df01..bf15faa56 100644 --- a/packages/payload/src/admin/components/elements/Card/index.tsx +++ b/packages/payload/src/admin/components/elements/Card/index.tsx @@ -1,45 +1,33 @@ -import React from 'react'; -import Button from '../Button'; -import { Props } from './types'; +import React from 'react' -import './index.scss'; +import type { Props } from './types' -const baseClass = 'card'; +import Button from '../Button' +import './index.scss' + +const baseClass = 'card' const Card: React.FC = (props) => { - const { id, title, titleAs, buttonAriaLabel, actions, onClick } = props; + const { actions, buttonAriaLabel, id, onClick, title, titleAs } = props - const classes = [ - baseClass, - id, - onClick && `${baseClass}--has-onclick`, - ].filter(Boolean).join(' '); + const classes = [baseClass, id, onClick && `${baseClass}--has-onclick`].filter(Boolean).join(' ') - const Tag = titleAs ?? 'div'; + const Tag = titleAs ?? 'div' return ( -
- - {title} - - {actions && ( -
- {actions} -
- )} +
+ {title} + {actions &&
{actions}
} {onClick && (
- ); -}; + ) +} -export default Card; +export default Card diff --git a/packages/payload/src/admin/components/elements/Card/types.ts b/packages/payload/src/admin/components/elements/Card/types.ts index fdacff58a..b6bf4f027 100644 --- a/packages/payload/src/admin/components/elements/Card/types.ts +++ b/packages/payload/src/admin/components/elements/Card/types.ts @@ -1,10 +1,10 @@ -import { ElementType } from 'react'; +import type { ElementType } from 'react' export type Props = { - id?: string, - title: string, - titleAs?: ElementType, - buttonAriaLabel?: string, - actions?: React.ReactNode, - onClick?: () => void, + actions?: React.ReactNode + buttonAriaLabel?: string + id?: string + onClick?: () => void + title: string + titleAs?: ElementType } diff --git a/packages/payload/src/admin/components/elements/CodeEditor/CodeEditor.tsx b/packages/payload/src/admin/components/elements/CodeEditor/CodeEditor.tsx index ca155cff5..2cd3b73ee 100644 --- a/packages/payload/src/admin/components/elements/CodeEditor/CodeEditor.tsx +++ b/packages/payload/src/admin/components/elements/CodeEditor/CodeEditor.tsx @@ -1,46 +1,47 @@ -import React from 'react'; -import Editor from '@monaco-editor/react'; -import type { Props } from './types'; -import { useTheme } from '../../utilities/Theme'; -import { ShimmerEffect } from '../ShimmerEffect'; +import Editor from '@monaco-editor/react' +import React from 'react' -import './index.scss'; +import type { Props } from './types' -const baseClass = 'code-editor'; +import { useTheme } from '../../utilities/Theme' +import { ShimmerEffect } from '../ShimmerEffect' +import './index.scss' + +const baseClass = 'code-editor' const CodeEditor: React.FC = (props) => { - const { readOnly, className, options, height, ...rest } = props; + const { className, height, options, readOnly, ...rest } = props - const { theme } = useTheme(); + const { theme } = useTheme() const classes = [ baseClass, className, rest?.defaultLanguage ? `language--${rest.defaultLanguage}` : '', - ].filter(Boolean).join(' '); + ] + .filter(Boolean) + .join(' ') return ( } - options={ - { - detectIndentation: true, - minimap: { - enabled: false, - }, - readOnly: Boolean(readOnly), - scrollBeyondLastLine: false, - tabSize: 2, - wordWrap: 'on', - ...options, - } - } height={height} + loading={} + theme={theme === 'dark' ? 'vs-dark' : 'vs'} {...rest} /> - ); -}; + ) +} -export default CodeEditor; +export default CodeEditor diff --git a/packages/payload/src/admin/components/elements/CodeEditor/index.tsx b/packages/payload/src/admin/components/elements/CodeEditor/index.tsx index 140317f18..e281d1c9c 100644 --- a/packages/payload/src/admin/components/elements/CodeEditor/index.tsx +++ b/packages/payload/src/admin/components/elements/CodeEditor/index.tsx @@ -1,18 +1,17 @@ -import React, { Suspense, lazy } from 'react'; -import { ShimmerEffect } from '../ShimmerEffect'; -import { Props } from './types'; +import React, { Suspense, lazy } from 'react' -const LazyEditor = lazy(() => import('./CodeEditor')); +import type { Props } from './types' + +import { ShimmerEffect } from '../ShimmerEffect' + +const LazyEditor = lazy(() => import('./CodeEditor')) export const CodeEditor: React.FC = (props) => { - const { height = '35vh' } = props; + const { height = '35vh' } = props return ( }> - + - ); -}; + ) +} diff --git a/packages/payload/src/admin/components/elements/CodeEditor/types.ts b/packages/payload/src/admin/components/elements/CodeEditor/types.ts index 073e121f5..6914cdc22 100644 --- a/packages/payload/src/admin/components/elements/CodeEditor/types.ts +++ b/packages/payload/src/admin/components/elements/CodeEditor/types.ts @@ -1,4 +1,4 @@ -import type { EditorProps } from '@monaco-editor/react'; +import type { EditorProps } from '@monaco-editor/react' export type Props = EditorProps & { readOnly?: boolean diff --git a/packages/payload/src/admin/components/elements/Collapsible/index.scss b/packages/payload/src/admin/components/elements/Collapsible/index.scss index 8dfd6da1e..1c7f978bf 100644 --- a/packages/payload/src/admin/components/elements/Collapsible/index.scss +++ b/packages/payload/src/admin/components/elements/Collapsible/index.scss @@ -1,8 +1,8 @@ @import '../../../scss/styles.scss'; .collapsible { - --toggle-pad-h: #{base(.75)}; - --toggle-pad-v: #{base(.5)}; + --toggle-pad-h: #{base(0.75)}; + --toggle-pad-v: #{base(0.5)}; border-radius: $style-radius-m; @@ -15,11 +15,11 @@ } &__drag { - opacity: .5; + opacity: 0.5; position: absolute; z-index: 1; top: var(--toggle-pad-v); - left: base(.5); + left: base(0.5); } &__toggle { @@ -43,7 +43,7 @@ border: 1px solid var(--theme-elevation-300); } - >.collapsible__toggle-wrap { + > .collapsible__toggle-wrap { .row-label { color: var(--theme-text); } @@ -52,7 +52,7 @@ } } &.collapsible--hovered { - >.collapsible__toggle-wrap .collapsible__toggle { + > .collapsible__toggle-wrap .collapsible__toggle { background: var(--theme-elevation-100); } } @@ -71,13 +71,13 @@ left: 0; pointer-events: none; - >* { + > * { pointer-events: all; } } &__header-wrap--has-drag-handle { - left: base(.875); + left: base(0.875); } &--collapsed { @@ -104,7 +104,7 @@ } &__indicator { - transform: rotate(.5turn); + transform: rotate(0.5turn); } &__content { @@ -121,7 +121,7 @@ } } -html[data-theme=dark] { +html[data-theme='dark'] { .collapsible { &--style-error { border: 1px solid var(--theme-error-400); @@ -129,7 +129,7 @@ html[data-theme=dark] { border: 1px solid var(--theme-error-500); } - >.collapsible__toggle-wrap { + > .collapsible__toggle-wrap { .row-label { color: var(--theme-error-500); } @@ -138,7 +138,7 @@ html[data-theme=dark] { } } &.collapsible--hovered { - >.collapsible__toggle-wrap .collapsible__toggle { + > .collapsible__toggle-wrap .collapsible__toggle { background: var(--theme-error-150); } } @@ -146,7 +146,7 @@ html[data-theme=dark] { } } -html[data-theme=light] { +html[data-theme='light'] { .collapsible { &--style-error { border: 1px solid var(--theme-error-500); @@ -154,7 +154,7 @@ html[data-theme=light] { border: 1px solid var(--theme-error-600); } - >.collapsible__toggle-wrap { + > .collapsible__toggle-wrap { .row-label { color: var(--theme-error-750); } @@ -163,7 +163,7 @@ html[data-theme=light] { } } &.collapsible--hovered { - >.collapsible__toggle-wrap .collapsible__toggle { + > .collapsible__toggle-wrap .collapsible__toggle { background: var(--theme-error-100); } } diff --git a/packages/payload/src/admin/components/elements/Collapsible/index.tsx b/packages/payload/src/admin/components/elements/Collapsible/index.tsx index 147026b38..243ee1952 100644 --- a/packages/payload/src/admin/components/elements/Collapsible/index.tsx +++ b/packages/payload/src/admin/components/elements/Collapsible/index.tsx @@ -1,43 +1,47 @@ -import React, { useState } from 'react'; -import AnimateHeight from 'react-animate-height'; -import { useTranslation } from 'react-i18next'; -import { Props } from './types'; -import { CollapsibleProvider, useCollapsible } from './provider'; -import Chevron from '../../icons/Chevron'; -import DragHandle from '../../icons/Drag'; +import React, { useState } from 'react' +import AnimateHeight from 'react-animate-height' +import { useTranslation } from 'react-i18next' -import './index.scss'; +import type { Props } from './types' -const baseClass = 'collapsible'; +import Chevron from '../../icons/Chevron' +import DragHandle from '../../icons/Drag' +import './index.scss' +import { CollapsibleProvider, useCollapsible } from './provider' + +const baseClass = 'collapsible' export const Collapsible: React.FC = ({ + actions, children, - collapsed: collapsedFromProps, - onToggle, className, + collapsed: collapsedFromProps, + collapsibleStyle = 'default', + dragHandleProps, header, initCollapsed, - dragHandleProps, - actions, - collapsibleStyle = 'default', + onToggle, }) => { - const [collapsedLocal, setCollapsedLocal] = useState(Boolean(initCollapsed)); - const [hoveringToggle, setHoveringToggle] = useState(false); - const isNested = useCollapsible(); - const { t } = useTranslation('fields'); + const [collapsedLocal, setCollapsedLocal] = useState(Boolean(initCollapsed)) + const [hoveringToggle, setHoveringToggle] = useState(false) + const isNested = useCollapsible() + const { t } = useTranslation('fields') - const collapsed = typeof collapsedFromProps === 'boolean' ? collapsedFromProps : collapsedLocal; + const collapsed = typeof collapsedFromProps === 'boolean' ? collapsedFromProps : collapsedLocal return ( -
= ({
)} {header && ( -
{header && header}
)}
- {actions && ( -
- {actions} -
- )} + {actions &&
{actions}
}
- -
- {children} -
+ +
{children}
- ); -}; + ) +} diff --git a/packages/payload/src/admin/components/elements/Collapsible/provider.tsx b/packages/payload/src/admin/components/elements/Collapsible/provider.tsx index 8f44f61a3..cf6db8b5e 100644 --- a/packages/payload/src/admin/components/elements/Collapsible/provider.tsx +++ b/packages/payload/src/admin/components/elements/Collapsible/provider.tsx @@ -1,17 +1,14 @@ -import React, { - createContext, useContext, -} from 'react'; +import React, { createContext, useContext } from 'react' -const Context = createContext(false); +const Context = createContext(false) -export const CollapsibleProvider: React.FC<{ children?: React.ReactNode, withinCollapsible?: boolean }> = ({ children, withinCollapsible = true }) => { - return ( - - {children} - - ); -}; +export const CollapsibleProvider: React.FC<{ + children?: React.ReactNode + withinCollapsible?: boolean +}> = ({ children, withinCollapsible = true }) => { + return {children} +} -export const useCollapsible = (): boolean => useContext(Context); +export const useCollapsible = (): boolean => useContext(Context) -export default Context; +export default Context diff --git a/packages/payload/src/admin/components/elements/Collapsible/types.ts b/packages/payload/src/admin/components/elements/Collapsible/types.ts index 3c5545ff9..5bfc5e4d1 100644 --- a/packages/payload/src/admin/components/elements/Collapsible/types.ts +++ b/packages/payload/src/admin/components/elements/Collapsible/types.ts @@ -1,14 +1,15 @@ -import React from 'react'; -import { DragHandleProps } from '../DraggableSortable/DraggableSortableItem/types'; +import type React from 'react' + +import type { DragHandleProps } from '../DraggableSortable/DraggableSortableItem/types' export type Props = { - collapsed?: boolean - className?: string - header?: React.ReactNode actions?: React.ReactNode children: React.ReactNode - onToggle?: (collapsed: boolean) => void - initCollapsed?: boolean - dragHandleProps?: DragHandleProps + className?: string + collapsed?: boolean collapsibleStyle?: 'default' | 'error' + dragHandleProps?: DragHandleProps + header?: React.ReactNode + initCollapsed?: boolean + onToggle?: (collapsed: boolean) => void } diff --git a/packages/payload/src/admin/components/elements/ColumnSelector/index.scss b/packages/payload/src/admin/components/elements/ColumnSelector/index.scss index f4ba08388..17c35e6cb 100644 --- a/packages/payload/src/admin/components/elements/ColumnSelector/index.scss +++ b/packages/payload/src/admin/components/elements/ColumnSelector/index.scss @@ -4,11 +4,11 @@ display: flex; flex-wrap: wrap; background: var(--theme-elevation-50); - padding: base(1) base(1) base(.5); + padding: base(1) base(1) base(0.5); &__column { - margin-right: base(.5); - margin-bottom: base(.5); + margin-right: base(0.5); + margin-bottom: base(0.5); background-color: transparent; box-shadow: 0 0 0 1px var(--theme-elevation-200); diff --git a/packages/payload/src/admin/components/elements/ColumnSelector/index.tsx b/packages/payload/src/admin/components/elements/ColumnSelector/index.tsx index 506c56636..60602acb9 100644 --- a/packages/payload/src/admin/components/elements/ColumnSelector/index.tsx +++ b/packages/payload/src/admin/components/elements/ColumnSelector/index.tsx @@ -1,78 +1,69 @@ -import React, { useId } from 'react'; -import { useTranslation } from 'react-i18next'; -import Pill from '../Pill'; -import Plus from '../../icons/Plus'; -import X from '../../icons/X'; -import { Props } from './types'; -import { getTranslation } from '../../../../utilities/getTranslation'; -import { useEditDepth } from '../../utilities/EditDepth'; -import DraggableSortable from '../DraggableSortable'; -import { useTableColumns } from '../TableColumns'; +import React, { useId } from 'react' +import { useTranslation } from 'react-i18next' -import './index.scss'; +import type { Props } from './types' -const baseClass = 'column-selector'; +import { getTranslation } from '../../../../utilities/getTranslation' +import Plus from '../../icons/Plus' +import X from '../../icons/X' +import { useEditDepth } from '../../utilities/EditDepth' +import DraggableSortable from '../DraggableSortable' +import Pill from '../Pill' +import { useTableColumns } from '../TableColumns' +import './index.scss' + +const baseClass = 'column-selector' const ColumnSelector: React.FC = (props) => { - const { - collection, - } = props; + const { collection } = props - const { - columns, - toggleColumn, - moveColumn, - } = useTableColumns(); + const { columns, moveColumn, toggleColumn } = useTableColumns() - const { i18n } = useTranslation(); - const uuid = useId(); - const editDepth = useEditDepth(); + const { i18n } = useTranslation() + const uuid = useId() + const editDepth = useEditDepth() - if (!columns) { return null; } + if (!columns) { + return null + } return ( col.accessor)} onDragEnd={({ moveFromIndex, moveToIndex }) => { moveColumn({ fromIndex: moveFromIndex, toIndex: moveToIndex, - }); + }) }} + className={baseClass} + ids={columns.map((col) => col.accessor)} > {columns.map((col, i) => { - const { - accessor, - active, - label, - name, - } = col; + const { accessor, active, label, name } = col - if (col.accessor === '_select') return null; + if (col.accessor === '_select') return null return ( { - toggleColumn(accessor); + toggleColumn(accessor) }} alignIcon="left" - key={`${collection.slug}-${col.name || i}${editDepth ? `-${editDepth}-` : ''}${uuid}`} - icon={active ? : } aria-checked={active} - className={[ - `${baseClass}__column`, - active && `${baseClass}__column--active`, - ].filter(Boolean).join(' ')} + draggable + icon={active ? : } + id={accessor} + key={`${collection.slug}-${col.name || i}${editDepth ? `-${editDepth}-` : ''}${uuid}`} > {getTranslation(label || name, i18n)} - ); + ) })} - ); -}; + ) +} -export default ColumnSelector; +export default ColumnSelector diff --git a/packages/payload/src/admin/components/elements/ColumnSelector/types.ts b/packages/payload/src/admin/components/elements/ColumnSelector/types.ts index 93b7866c0..39ea4e291 100644 --- a/packages/payload/src/admin/components/elements/ColumnSelector/types.ts +++ b/packages/payload/src/admin/components/elements/ColumnSelector/types.ts @@ -1,5 +1,5 @@ -import { SanitizedCollectionConfig } from '../../../../collections/config/types'; +import type { SanitizedCollectionConfig } from '../../../../collections/config/types' export type Props = { - collection: SanitizedCollectionConfig, + collection: SanitizedCollectionConfig } diff --git a/packages/payload/src/admin/components/elements/CopyToClipboard/index.tsx b/packages/payload/src/admin/components/elements/CopyToClipboard/index.tsx index 6d7a18f9d..d683d1068 100644 --- a/packages/payload/src/admin/components/elements/CopyToClipboard/index.tsx +++ b/packages/payload/src/admin/components/elements/CopyToClipboard/index.tsx @@ -1,64 +1,53 @@ -import React, { useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import Copy from '../../icons/Copy'; -import Tooltip from '../Tooltip'; -import { Props } from './types'; +import React, { useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' -import './index.scss'; +import type { Props } from './types' -const baseClass = 'copy-to-clipboard'; +import Copy from '../../icons/Copy' +import Tooltip from '../Tooltip' +import './index.scss' -const CopyToClipboard: React.FC = ({ - value, - defaultMessage, - successMessage, -}) => { - const ref = useRef(null); - const [copied, setCopied] = useState(false); - const [hovered, setHovered] = useState(false); - const { t } = useTranslation('general'); +const baseClass = 'copy-to-clipboard' + +const CopyToClipboard: React.FC = ({ defaultMessage, successMessage, value }) => { + const ref = useRef(null) + const [copied, setCopied] = useState(false) + const [hovered, setHovered] = useState(false) + const { t } = useTranslation('general') if (value) { return (