Compare commits

..

1 Commits

Author SHA1 Message Date
Dan Ribbens
3dd28e41d8 POC infinite recursion of blocks 2024-04-11 12:28:20 -04:00
211 changed files with 974 additions and 5689 deletions

46
.github/CODEOWNERS vendored
View File

@@ -1,32 +1,50 @@
# Order matters. The last matching pattern takes precedence.
### Catch-all ###
* @denolfe @jmikrut @DanRibbens
.* @denolfe @jmikrut @DanRibbens
### Core ###
/packages/payload/ @denolfe @jmikrut @DanRibbens
/packages/payload/src/uploads/ @denolfe
/packages/payload/src/admin/ @jmikrut @jacobsfletch @JarrodMFlesch
### Adapters ###
/packages/richtext-*/ @AlessioGr
/packages/bundler-*/ @denolfe @jmikrut @DanRibbens @JarrodMFlesch
/packages/db-*/ @denolfe @jmikrut @DanRibbens
/packages/richtext-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
### Plugins ###
/packages/plugin-*/ @denolfe @jmikrut @DanRibbens @jacobsfletch @JarrodMFlesch @AlessioGr
/packages/plugin-cloud*/ @denolfe
/packages/plugin-form-builder/ @jacobsfletch
/packages/plugin-live-preview*/ @jacobsfletch
/packages/plugin-nested-docs/ @jacobsfletch
/packages/plugin-password-protection/ @jmikrut
/packages/plugin-redirects/ @jacobsfletch
/packages/plugin-search/ @jacobsfletch
/packages/plugin-sentry/ @JessChowdhury
/packages/plugin-seo/ @jacobsfletch
/packages/plugin-stripe/ @jacobsfletch
/packages/plugin-zapier/ @JarrodMFlesch
### Examples ###
/examples/ @jacobsfletch
/examples/testing/ @JarrodMFlesch
/examples/email/ @JessChowdhury
/examples/whitelabel/ @JessChowdhury
### Templates ###
/templates/ @jacobsfletch @denolfe
/templates/ @jacobsfletch
/templates/blank/ @denolfe
### Misc ###
/packages/create-payload-app/ @denolfe
/packages/eslint-*/ @denolfe
### Build Files ###
/**/package.json @denolfe
/tsconfig.json @denolfe
/**/tsconfig*.json @denolfe
/jest.config.js @denolfe
/**/jest.config.js @denolfe
/packages/eslint-config-payload/ @denolfe
/packages/payload-admin-bar/ @jacobsfletch
### Root ###
/package.json @denolfe
/scripts/ @denolfe
/.husky/ @denolfe
/.vscode/ @denolfe
/.github/ @denolfe
/.github/CODEOWNERS @denolfe

View File

@@ -15,10 +15,6 @@ jobs:
needs_build: ${{ steps.filter.outputs.needs_build }}
templates: ${{ steps.filter.outputs.templates }}
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- uses: actions/checkout@v4
with:
fetch-depth: 25
@@ -49,17 +45,13 @@ jobs:
with:
fetch-depth: 25
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Use Node.js 20
uses: actions/setup-node@v4
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
@@ -69,7 +61,7 @@ jobs:
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
@@ -82,7 +74,7 @@ jobs:
- run: pnpm run build
- name: Cache build
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -104,23 +96,19 @@ jobs:
AWS_REGION: us-east-1
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Use Node.js 20
uses: actions/setup-node@v4
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -189,23 +177,19 @@ jobs:
part: [ 1/8, 2/8, 3/8, 4/8, 5/8, 6/8, 7/8, 8/8 ]
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Use Node.js 20
uses: actions/setup-node@v4
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -230,23 +214,19 @@ jobs:
needs: core-build
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Use Node.js 20
uses: actions/setup-node@v4
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -274,23 +254,19 @@ jobs:
- live-preview-react
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Use Node.js 20
uses: actions/setup-node@v4
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -315,23 +291,19 @@ jobs:
- plugin-seo
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Use Node.js 20
uses: actions/setup-node@v4
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -357,14 +329,10 @@ jobs:
with:
fetch-depth: 25
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Use Node.js 20
uses: actions/setup-node@v4
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 18
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.10.0

View File

@@ -1,11 +0,0 @@
name: pr-title
on:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Echo
run: echo "Register pr-title workflow"

View File

@@ -5,21 +5,21 @@
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
"source.fixAll.eslint": true
}
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
"source.fixAll.eslint": true
}
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
"source.fixAll.eslint": true
}
},
"[json]": {

View File

@@ -1,77 +1,3 @@
## [2.14.1](https://github.com/payloadcms/payload/compare/v2.14.0...v2.14.1) (2024-04-25)
### Bug Fixes
* **db-postgres:** cumulative updates ([#6033](https://github.com/payloadcms/payload/issues/6033)) ([c31b8dc](https://github.com/payloadcms/payload/commit/c31b8dcaa0c43132d8a01e0cc43094f466cc9168))
* disable api key checkbox does not remove api key ([#6017](https://github.com/payloadcms/payload/issues/6017)) ([0ffdcc6](https://github.com/payloadcms/payload/commit/0ffdcc685f4e917a02e62dbaccec7cc8ebbf695d))
* **richtext-lexical:** minimize the amount of times sanitizeFields is called ([#6018](https://github.com/payloadcms/payload/issues/6018)) ([60372fa](https://github.com/payloadcms/payload/commit/60372faf36b7f6d92a61ccbaee0f528e50f5a51a))
## [2.14.0](https://github.com/payloadcms/payload/compare/v2.13.0...v2.14.0) (2024-04-24)
### Features
* add count operation to collections ([#5936](https://github.com/payloadcms/payload/issues/5936)) ([c380dee](https://github.com/payloadcms/payload/commit/c380deee4a1db82bce9fea264060000957a53eee))
### Bug Fixes
* bulk publish ([#6006](https://github.com/payloadcms/payload/issues/6006)) ([c11600a](https://github.com/payloadcms/payload/commit/c11600aac38cd67019765faf2a41e62df13e50cc))
* **db-postgres:** extra version suffix added to table names ([#5939](https://github.com/payloadcms/payload/issues/5939)) ([bd8b512](https://github.com/payloadcms/payload/commit/bd8b5123b0991e53eb209315897dbca10d14d45e))
* **db-postgres:** Fixes nested groups inside nested blocks ([#5882](https://github.com/payloadcms/payload/issues/5882)) ([e258866](https://github.com/payloadcms/payload/commit/e25886649fce414d5d47918f35ba2d4d2ba59174))
* **db-postgres:** row table names were not being built properly - v2 ([#5961](https://github.com/payloadcms/payload/issues/5961)) ([9152a23](https://github.com/payloadcms/payload/commit/9152a238d2982503e7f509350651b0ba3f83b1ec))
* header filters ([#5997](https://github.com/payloadcms/payload/issues/5997)) ([ad01c67](https://github.com/payloadcms/payload/commit/ad01c6784d283386dc819dfcd47455cad5accfaa))
* min/max attributes missing from number input ([#5779](https://github.com/payloadcms/payload/issues/5779)) ([985796b](https://github.com/payloadcms/payload/commit/985796be54b593af0a4934685ab8621b9badda10))
* removes `equals` & `not_equals` operators from fields with `hasMany` ([#5885](https://github.com/payloadcms/payload/issues/5885)) ([a8c9625](https://github.com/payloadcms/payload/commit/a8c9625cdec33476a5da87bcd9f010f9d7fb9a94))
## [2.13.0](https://github.com/payloadcms/payload/compare/v2.12.1...v2.13.0) (2024-04-19)
### Features
* allow configuration for setting headers on external file fetch ([ec1ad0b](https://github.com/payloadcms/payload/commit/ec1ad0b6628d400d7435821c8a72b6746bf87577))
* **db-\*:** custom db table and enum names ([#5045](https://github.com/payloadcms/payload/issues/5045)) ([9bbacc4](https://github.com/payloadcms/payload/commit/9bbacc4fb1ad247634f394e95c42ee3adade8048))
* json field schemas ([#5726](https://github.com/payloadcms/payload/issues/5726)) ([2c402cc](https://github.com/payloadcms/payload/commit/2c402cc65c9e8f7f33e2fb0ce5e1a8ceff52af1b))
* **plugin-seo:** add Chinese translation ([#5429](https://github.com/payloadcms/payload/issues/5429)) ([fcb29bb](https://github.com/payloadcms/payload/commit/fcb29bb1c637867301bbc1070b4a84383bf0e90a))
* **richtext-lexical:** add HorizontalRuleFeature ([d8e9084](https://github.com/payloadcms/payload/commit/d8e9084db21828968046ab59775633e409ce5c2a))
* **richtext-lexical:** improve floating handle y-positioning by positioning it in the center for smaller elements. ([0055a8e](https://github.com/payloadcms/payload/commit/0055a8eb36b95722cccdc5eb3101a79d3e764f8b))
### Bug Fixes
* adds type error validations for `email` and `password` in login operation ([#4852](https://github.com/payloadcms/payload/issues/4852)) ([1f00360](https://github.com/payloadcms/payload/commit/1f0036054a9461535b0992f2449e91e4eaf97d4e))
* avoids getting and setting doc preferences when creating new ([#5757](https://github.com/payloadcms/payload/issues/5757)) ([e3c3dda](https://github.com/payloadcms/payload/commit/e3c3ddac34dff8fa085f5b702be2838d513be300))
* block field type missing dbName ([#5695](https://github.com/payloadcms/payload/issues/5695)) ([e7608f5](https://github.com/payloadcms/payload/commit/e7608f5507d3b85ea3f44b5cb1f43edf67608b1b))
* **db-mongodb:** failing `contains` query with special chars ([#5774](https://github.com/payloadcms/payload/issues/5774)) ([5fa99fb](https://github.com/payloadcms/payload/commit/5fa99fb060cabbb69b5d6688748260e562e6bea3))
* **db-mongodb:** ignore end session errors ([#5904](https://github.com/payloadcms/payload/issues/5904)) ([cb8d562](https://github.com/payloadcms/payload/commit/cb8d562132bee437798880e1d7f64dbfdee36949))
* **db-mongodb:** version fields indexSortableFields ([#5863](https://github.com/payloadcms/payload/issues/5863)) ([fe0028c](https://github.com/payloadcms/payload/commit/fe0028c89945303a431b48efdae7b6e22304c8a3))
* **db-postgres:** hasMany relationship query contains operator ([#4212](https://github.com/payloadcms/payload/issues/4212)) ([608d6d0](https://github.com/payloadcms/payload/commit/608d6d0a872af224ea42c3e6c8a3b4f21678f550))
* **db-postgres:** issue querying by localised relationship not respecting locale as constraint ([#5666](https://github.com/payloadcms/payload/issues/5666)) ([44599cb](https://github.com/payloadcms/payload/commit/44599cbc7b8f23d6d8c7a3e05466237406812a6d))
* **db-postgres:** query hasMany fields with in ([#5881](https://github.com/payloadcms/payload/issues/5881)) ([6185f8a](https://github.com/payloadcms/payload/commit/6185f8a5d845d12651f5a3ee128eb43d3b9d2449))
* **db-postgres:** relationship query pagination ([#5802](https://github.com/payloadcms/payload/issues/5802)) ([65690a6](https://github.com/payloadcms/payload/commit/65690a675c17cfacebe775a327a57741ac09416a))
* **db-postgres:** validateExistingBlockIsIdentical localized ([#5839](https://github.com/payloadcms/payload/issues/5839)) ([4c4f924](https://github.com/payloadcms/payload/commit/4c4f924e90ee23a73c9a7cc7e69bbc2caf902b92))
* duplicate document multiple times in quick succession ([#5642](https://github.com/payloadcms/payload/issues/5642)) ([373787d](https://github.com/payloadcms/payload/commit/373787de31cbbd33b587aa4be6344948f082f5bb))
* missing date locales ([#5656](https://github.com/payloadcms/payload/issues/5656)) ([c1c8600](https://github.com/payloadcms/payload/commit/c1c86009a5e9aad401a05f7c63ad37bd3f88dc84))
* number ids were not sanitized to number in rest api ([51f84a4](https://github.com/payloadcms/payload/commit/51f84a4fcfd437eb73c7d83205b66e3620085909))
* passes parent id instead of incoming id to saveVersion ([#5831](https://github.com/payloadcms/payload/issues/5831)) ([25c9a14](https://github.com/payloadcms/payload/commit/25c9a145bec9e9566d2bbcba59d5b34394e10bbd))
* **plugin-seo:** uses correct key for ukrainian translation ([#5873](https://github.com/payloadcms/payload/issues/5873)) ([e47e544](https://github.com/payloadcms/payload/commit/e47e544364031ac834565a4d86ef6ec9c04e63c0))
* properly handle drafts in bulk update ([#5872](https://github.com/payloadcms/payload/issues/5872)) ([ad38f76](https://github.com/payloadcms/payload/commit/ad38f760111abf947c6b0ee4b983ee1224a9bf1b))
* req.collection being lost when querying a global inside a collection ([#5727](https://github.com/payloadcms/payload/issues/5727)) ([cbd03ed](https://github.com/payloadcms/payload/commit/cbd03ed2f8819ee8ac20e8739cc03e88ff4caa25))
* **richtext-lexical:** catch errors that may occur during HTML generation ([#5754](https://github.com/payloadcms/payload/issues/5754)) ([9b44296](https://github.com/payloadcms/payload/commit/9b442960929d00faa07f1383b1267f71e6f44efe))
* **richtext-lexical:** do not allow omitting editor prop for sub-richtext fields within lexical defined in the payload config ([#5766](https://github.com/payloadcms/payload/issues/5766)) ([6186493](https://github.com/payloadcms/payload/commit/6186493246157b4d4b33c8c47378f08581315942))
* **richtext-lexical:** incorrect floating handle y-position calculation next to certain kinds of HTML elements like HR ([de5d6cc](https://github.com/payloadcms/payload/commit/de5d6cc4bd591745156f0b8c56795b7bd2eaad7e))
* **richtext-lexical:** limit unnecessary floating handle positioning updates ([a00439e](https://github.com/payloadcms/payload/commit/a00439ea893e074d64be83ee6af1e780178a7ee3))
* **richtext-lexical:** pass through config for schema generation. Makes it more robust ([#5700](https://github.com/payloadcms/payload/issues/5700)) ([cf135fd](https://github.com/payloadcms/payload/commit/cf135fd1e4aeb30121281399e26be901393ada6d))
* **richtext-lexical:** use correct nodeType on HorizontalRule feature HTML converter ([#5805](https://github.com/payloadcms/payload/issues/5805)) ([3b1d331](https://github.com/payloadcms/payload/commit/3b1d3313165499616673f6d363c90ef884994525))
* updates type name of `CustomPublishButtonProps` to `CustomPublishButtonType` ([#5644](https://github.com/payloadcms/payload/issues/5644)) ([7df7bf4](https://github.com/payloadcms/payload/commit/7df7bf448bd26e870a1fde8aaa47430904d68366))
* updates var ([9530d28](https://github.com/payloadcms/payload/commit/9530d28a6760a667b718027a49ea43ba1accd546))
* use isolateObjectProperty function in createLocalReq ([#5748](https://github.com/payloadcms/payload/issues/5748)) ([c0ba6cc](https://github.com/payloadcms/payload/commit/c0ba6cc19a20c043a08ca77caacd47ef7cfb48f4))
* uses find instead of fieldIndex for custom ID check ([509ec67](https://github.com/payloadcms/payload/commit/509ec677c42993d9c08facf6928a5ef1e9767508))
### ⚠ BREAKING CHANGES
* **richtext-lexical:** do not allow omitting editor prop for sub-richtext fields within lexical defined in the payload config ([#5766](https://github.com/payloadcms/payload/issues/5766))
## [2.12.1](https://github.com/payloadcms/payload/compare/v2.12.0...v2.12.1) (2024-04-03)

View File

@@ -17,7 +17,7 @@
<hr/>
> [!IMPORTANT]
> 🎉 <strong>Payload 3.0 beta released!</strong> You can now deploy Payload fully in any Next.js app folder. Read more in the <a target="_blank" href="https://payloadcms.com/blog/30-beta-install-payload-into-any-nextjs-app-with-one-line" rel="dofollow"><strong>announcement post</strong></a>.
> 🎉 <strong>Payload 2.0 is now available!</strong> Read more in the <a target="_blank" href="https://payloadcms.com/blog/payload-2-0" rel="dofollow"><strong>announcement post</strong></a>.
<h3>Benefits over a regular CMS</h3>
<ul>

View File

@@ -657,7 +657,7 @@ As your admin customizations gets more complex you may want to share state betwe
### Styling Custom Components
Payload exports its SCSS variables and mixins for reuse in your own custom components. This is helpful in cases where you might want to style a custom input similarly to Payload's built-in styling, so it blends more thoroughly into the existing admin UI.
Payload exports its SCSS variables and mixins for reuse in your own custom components. This is helpful in cases where you might want to style a custom input similarly to Payload's built-ini styling, so it blends more thoroughly into the existing admin UI.
To make use of Payload SCSS variables / mixins to use directly in your own components, you can import them as follows:

View File

@@ -4,7 +4,7 @@ label: JSON
order: 50
desc: The JSON field type will store any string in the Database. Learn how to use JSON fields, see examples and options.
keywords: json, jsonSchema, schema, validation, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
keywords: json, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
---
<Banner>
@@ -30,7 +30,6 @@ This field uses the `monaco-react` editor syntax highlighting.
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
| **`jsonSchema`** | Provide a JSON schema that will be used for validation. [JSON schemas](https://json-schema.org/learn/getting-started-step-by-step) |
| **`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) |
@@ -53,7 +52,7 @@ In addition to the default [field admin config](/docs/fields/overview#admin-conf
### Example
`collections/ExampleCollection.ts`
`collections/ExampleCollection.ts
```ts
import { CollectionConfig } from 'payload/types'
@@ -69,68 +68,3 @@ export const ExampleCollection: CollectionConfig = {
],
}
```
### JSON Schema Validation
Payload JSON fields fully support the [JSON schema](https://json-schema.org/) standard. By providing a schema in your field config, the editor will be guided in the admin UI, getting typeahead for properties and their formats automatically. When the document is saved, the default validation will prevent saving any invalid data in the field according to the schema in your config.
If you only provide a URL to a schema, Payload will fetch the desired schema if it is publicly available. If not, it is recommended to add the schema directly to your config or import it from another file so that it can be implemented consistently in your project.
#### Local JSON Schema
`collections/ExampleCollection.ts`
```ts
import { CollectionConfig } from 'payload/types'
export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
fields: [
{
name: 'customerJSON', // required
type: 'json', // required
jsonSchema: {
uri: 'a://b/foo.json', // required
fileMatch: ['a://b/foo.json'], // required
schema: {
type: 'object',
properties: {
foo: {
enum: ['bar', 'foobar'],
}
},
},
},
},
],
}
// {"foo": "bar"} or {"foo": "foobar"} - ok
// Attempting to create {"foo": "not-bar"} will throw an error
```
#### Remote JSON Schema
`collections/ExampleCollection.ts`
```ts
import { CollectionConfig } from 'payload/types'
export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
fields: [
{
name: 'customerJSON', // required
type: 'json', // required
jsonSchema: {
uri: 'https://example.com/customer.schema.json', // required
fileMatch: ['https://example.com/customer.schema.json'], // required
},
},
],
}
// If 'https://example.com/customer.schema.json' has a JSON schema
// {"foo": "bar"} or {"foo": "foobar"} - ok
// Attempting to create {"foo": "not-bar"} will throw an error
```

View File

@@ -43,12 +43,11 @@ export const PublicUser: CollectionConfig = {
**Payload will automatically open up the following queries:**
| Query Name | Operation |
| ------------------ | ------------------- |
| **`PublicUser`** | `findByID` |
| **`PublicUsers`** | `find` |
| **`countPublicUsers`** | `count` |
| **`mePublicUser`** | `me` auth operation |
| Query Name | Operation |
| ------------------ | ------------------- |
| **`PublicUser`** | `findByID` |
| **`PublicUsers`** | `find` |
| **`mePublicUser`** | `me` auth operation |
**And the following mutations:**

View File

@@ -8,7 +8,7 @@ keywords: live preview, frontend, react, next.js, vue, nuxt.js, svelte, hook, us
While using Live Preview, the Admin panel emits a new `window.postMessage` event every time a change is made to the document. Your front-end application can listen for these events and re-render accordingly.
Wiring your front-end into Live Preview is easy. If your front-end application is built with React, Next.js, Vue or Nuxt.js, use the `useLivePreview` hook that Payload provides. In the future, all other major frameworks like Svelte will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own hook](#building-your-own-hook) for more information.
Wiring your front-end into Live Preview is easy. If your front-end application is built with React or Next.js, use the [`useLivePreview`](#react) React hook that Payload provides. In the future, all other major frameworks like Vue, Svelte, etc will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own hook](#building-your-own-hook) for more information.
By default, all hooks accept the following args:
@@ -32,10 +32,6 @@ And return the following values:
If your front-end is tightly coupled to required fields, you should ensure that your UI does not break when these fields are removed. For example, if you are rendering something like `data.relatedPosts[0].title`, your page will break once you remove the first related post. To get around this, use conditional logic, optional chaining, or default values in your UI where needed. For example, `data?.relatedPosts?.[0]?.title`.
</Banner>
<Banner type="info">
If is important that the `depth` argument matches exactly with the depth of your initial page request. The depth property is used to populated relationships and uploads beyond their IDs. See [Depth](../getting-started/concepts#depth) for more information.
</Banner>
### React
If your front-end application is built with React or Next.js, you can use the `useLivePreview` hook that Payload provides.
@@ -73,40 +69,9 @@ export const PageClient: React.FC<{
}
```
### Vue
If your front-end application is built with Vue 3 or Nuxt 3, you can use the `useLivePreview` composable that Payload provides.
First, install the `@payloadcms/live-preview-vue` package:
```bash
npm install @payloadcms/live-preview-vue
```
Then, use the `useLivePreview` hook in your Vue component:
```vue
<script setup lang="ts">
import type { PageData } from '~/types';
import { defineProps } from 'vue';
import { useLivePreview } from '@payloadcms/live-preview-vue';
// Fetch the initial data on the parent component or using async state
const props = defineProps<{ initialData: PageData }>();
// The hook will take over from here and keep the preview in sync with the changes you make.
// The `data` property will contain the live data of the document only when viewed from the Preview view of the Admin UI.
const { data } = useLivePreview<PageData>({
initialData: props.initialData,
serverURL: "<PAYLOAD_SERVER_URL>",
depth: 2,
});
</script>
<template>
<h1>{{ data.title }}</h1>
</template>
```
<Banner type="info">
If is important that the `depth` argument matches exactly with the depth of your initial page request. The depth property is used to populated relationships and uploads beyond their IDs. See [Depth](../getting-started/concepts#depth) for more information.
</Banner>
## Building your own hook

View File

@@ -164,22 +164,6 @@ const result = await payload.findByID({
})
```
#### Count
```js
// Result will be an object with:
// {
// totalDocs: 10, // count of the documents satisfies query
// }
const result = await payload.count({
collection: 'posts', // required
locale: 'en',
where: {}, // pass a `where` query here
user: dummyUser,
overrideAccess: false,
})
```
#### Update by ID
```js

View File

@@ -90,19 +90,6 @@ Note: Collection slugs must be formatted in kebab-case
},
},
},
{
operation: "Count",
method: "GET",
path: "/api/{collection-slug}/count",
description: "Count the documents",
example: {
slug: "count",
req: true,
res: {
totalDocs: 10
},
},
},
{
operation: "Create",
method: "POST",

View File

@@ -153,14 +153,13 @@ Here's an overview of all the included features:
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
| **`CheckListFeature`** | Yes | Adds checklists |
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
| **`BlockQuoteFeature`** | Yes | Allows you to create block-level quotes |
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |

View File

@@ -40,22 +40,21 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
### Collection Upload Options
| 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. |
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`externalFileHeaderFilter`** | Accepts existing headers and can filter/modify them. |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`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) |
| **`handlers`** | Array of Express request handlers to execute before the built-in Payload static middleware executes. |
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
| 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. |
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`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) |
| **`handlers`** | Array of Express request handlers to execute before the built-in Payload static middleware executes. |
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
_An asterisk denotes that a property above is required._

View File

@@ -128,7 +128,6 @@
]
},
"dependencies": {
"@sentry/react": "^7.77.0",
"ajv": "^8.12.0"
"@sentry/react": "^7.77.0"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "1.5.1",
"version": "1.4.4",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": {
"type": "git",

View File

@@ -1,49 +0,0 @@
import type { QueryOptions } from 'mongoose'
import type { Count } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import { flattenWhereToOperators } from 'payload/database'
import type { MongooseAdapter } from '.'
import { withSession } from './withSession'
export const count: Count = async function count(
this: MongooseAdapter,
{ collection, locale, req = {} as PayloadRequest, where },
) {
const Model = this.collections[collection]
const options: QueryOptions = withSession(this, req.transactionID)
let hasNearConstraint = false
if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
const query = await Model.buildQuery({
locale,
payload: this.payload,
where,
})
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
options.hint = {
_id: 1,
}
}
const result = await Model.countDocuments(query, options)
return {
totalDocs: result,
}
}

View File

@@ -11,7 +11,6 @@ import { createDatabaseAdapter } from 'payload/database'
import type { CollectionModel, GlobalModel } from './types'
import { connect } from './connect'
import { count } from './count'
import { create } from './create'
import { createGlobal } from './createGlobal'
import { createGlobalVersion } from './createGlobalVersion'
@@ -109,7 +108,6 @@ export function mongooseAdapter({
collections: {},
connectOptions: connectOptions || {},
connection: undefined,
count,
disableIndexHints,
globals: undefined,
mongoMemoryServer: undefined,
@@ -117,6 +115,7 @@ export function mongooseAdapter({
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
url,
versions: {},
// DatabaseAdapter
beginTransaction: transactionOptions ? beginTransaction : undefined,
commitTransaction,

View File

@@ -6,7 +6,11 @@ import type { SanitizedCollectionConfig } from 'payload/types'
import mongoose from 'mongoose'
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'
import paginate from 'mongoose-paginate-v2'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload/versions'
import {
buildVersionCollectionFields,
buildVersionGlobalFields,
getVersionsModelName,
} from 'payload/versions'
import type { MongooseAdapter } from '.'
import type { CollectionModel } from './types'
@@ -29,7 +33,6 @@ export const init: Init = async function init(this: MongooseAdapter) {
const versionSchema = buildSchema(this.payload.config, versionCollectionFields, {
disableUnique: true,
draftsEnabled: true,
indexSortableFields: this.payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: false,

View File

@@ -142,10 +142,7 @@ export const sanitizeQueryValue = ({
if (path !== '_id' || (path === '_id' && hasCustomID && field.type === 'text')) {
if (operator === 'contains') {
formattedValue = {
$options: 'i',
$regex: formattedValue.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'),
}
formattedValue = { $options: 'i', $regex: formattedValue }
}
}

View File

@@ -6,10 +6,6 @@ export const commitTransaction: CommitTransaction = async function commitTransac
}
await this.sessions[id].commitTransaction()
try {
await this.sessions[id].endSession()
} catch (error) {
// ending sessions is only best effort and won't impact anything if it fails since the transaction was committed
}
await this.sessions[id].endSession()
delete this.sessions[id]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "0.8.2",
"version": "0.7.1",
"description": "The officially supported Postgres database adapter for Payload",
"repository": {
"type": "git",

View File

@@ -1,62 +0,0 @@
import type { Count } from 'payload/database'
import type { SanitizedCollectionConfig } from 'payload/types'
import { sql } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { ChainedMethods } from './find/chainMethods'
import type { PostgresAdapter } from './types'
import { chainMethods } from './find/chainMethods'
import buildQuery from './queries/buildQuery'
export const count: Count = async function count(
this: PostgresAdapter,
{ collection, locale, req, where: whereArg },
) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
const db = this.sessions[req.transactionID]?.db || this.drizzle
const table = this.tables[tableName]
const { joinAliases, joins, where } = await buildQuery({
adapter: this,
fields: collectionConfig.fields,
locale,
tableName,
where: whereArg,
})
const selectCountMethods: ChainedMethods = []
joinAliases.forEach(({ condition, table }) => {
selectCountMethods.push({
args: [table, condition],
method: 'leftJoin',
})
})
Object.entries(joins).forEach(([joinTable, condition]) => {
if (joinTable) {
selectCountMethods.push({
args: [this.tables[joinTable], condition],
method: 'leftJoin',
})
}
})
const countResult = await chainMethods({
methods: selectCountMethods,
query: db
.select({
count: sql<number>`count
(DISTINCT ${this.tables[tableName].id})`,
})
.from(table)
.where(where),
})
return { totalDocs: Number(countResult[0].count) }
}

View File

@@ -2,8 +2,8 @@ import type { Create } from 'payload/database'
import type { PostgresAdapter } from './types'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow'
import toSnakeCase from 'to-snake-case'
export const create: Create = async function create(
this: PostgresAdapter,
@@ -12,8 +12,6 @@ export const create: Create = async function create(
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
const result = await upsertRow({
adapter: this,
data,
@@ -21,7 +19,10 @@ export const create: Create = async function create(
fields: collection.fields,
operation: 'create',
req,
tableName,
tableName: getTableName({
adapter: this,
config: collection,
}),
})
return result

View File

@@ -1,10 +1,9 @@
import type { CreateGlobalArgs } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow'
export async function createGlobal<T extends TypeWithID>(
@@ -14,8 +13,6 @@ export async function createGlobal<T extends TypeWithID>(
const db = this.sessions[req.transactionID]?.db || this.drizzle
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))
const result = await upsertRow<T>({
adapter: this,
data,
@@ -23,7 +20,10 @@ export async function createGlobal<T extends TypeWithID>(
fields: globalConfig.fields,
operation: 'create',
req,
tableName,
tableName: getTableName({
adapter: this,
config: globalConfig,
}),
})
return result

View File

@@ -4,10 +4,10 @@ import type { PayloadRequest, TypeWithID } from 'payload/types'
import { sql } from 'drizzle-orm'
import { type CreateGlobalVersionArgs } from 'payload/database'
import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow'
export async function createGlobalVersion<T extends TypeWithID>(
@@ -16,8 +16,11 @@ export async function createGlobalVersion<T extends TypeWithID>(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const global = this.payload.globals.config.find(({ slug }) => slug === globalSlug)
const tableName = this.tableNameMap.get(`_${toSnakeCase(global.slug)}${this.versionsSuffix}`)
const tableName = getTableName({
adapter: this,
config: global,
versions: true,
})
const result = await upsertRow<TypeWithVersion<T>>({
adapter: this,
@@ -37,9 +40,9 @@ export async function createGlobalVersion<T extends TypeWithID>(
if (global.versions.drafts) {
await db.execute(sql`
UPDATE ${table}
SET latest = false
WHERE ${table.id} != ${result.id};
UPDATE ${table}
SET latest = false
WHERE ${table.id} != ${result.id};
`)
}

View File

@@ -3,10 +3,10 @@ import type { PayloadRequest, TypeWithID } from 'payload/types'
import { sql } from 'drizzle-orm'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow'
export async function createVersion<T extends TypeWithID>(
@@ -21,12 +21,11 @@ export async function createVersion<T extends TypeWithID>(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const defaultTableName = toSnakeCase(collection.slug)
const tableName = this.tableNameMap.get(`_${defaultTableName}${this.versionsSuffix}`)
const version = { ...versionData }
if (version.id) delete version.id
const tableName = getTableName({
adapter: this,
config: collection,
versions: true,
})
const result = await upsertRow<TypeWithVersion<T>>({
adapter: this,
@@ -34,7 +33,7 @@ export async function createVersion<T extends TypeWithID>(
autosave,
latest: true,
parent,
version,
version: versionData,
},
db,
fields: buildVersionCollectionFields(collection),
@@ -44,19 +43,25 @@ export async function createVersion<T extends TypeWithID>(
})
const table = this.tables[tableName]
const relationshipsTable =
this.tables[`_${defaultTableName}${this.versionsSuffix}${this.relationshipsSuffix}`]
this.tables[
getTableName({
adapter: this,
config: collection,
relationships: true,
versions: true,
})
]
if (collection.versions.drafts) {
await db.execute(sql`
UPDATE ${table}
SET latest = false
FROM ${relationshipsTable}
WHERE ${table.id} = ${relationshipsTable.parent}
AND ${relationshipsTable.path} = ${'parent'}
AND ${relationshipsTable[`${collectionSlug}ID`]} = ${parent}
AND ${table.id} != ${result.id};
UPDATE ${table}
SET latest = false
FROM ${relationshipsTable}
WHERE ${table.id} = ${relationshipsTable.parent}
AND ${relationshipsTable.path} = ${'parent'}
AND ${relationshipsTable[`${collectionSlug}ID`]} = ${parent}
AND ${table.id} != ${result.id};
`)
}

View File

@@ -2,11 +2,11 @@ import type { DeleteMany } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import { inArray } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const deleteMany: DeleteMany = async function deleteMany(
this: PostgresAdapter,
@@ -14,8 +14,7 @@ export const deleteMany: DeleteMany = async function deleteMany(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig = this.payload.collections[collection].config
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
const tableName = getTableName({ adapter: this, config: collectionConfig })
const result = await findMany({
adapter: this,

View File

@@ -2,13 +2,13 @@ import type { DeleteOne } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import { eq } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { buildFindManyArgs } from './find/buildFindManyArgs'
import buildQuery from './queries/buildQuery'
import { selectDistinct } from './queries/selectDistinct'
import { getTableName } from './schema/getTableName'
import { transform } from './transform/read'
export const deleteOne: DeleteOne = async function deleteOne(
@@ -17,9 +17,10 @@ export const deleteOne: DeleteOne = async function deleteOne(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
const tableName = getTableName({
adapter: this,
config: collection,
})
let docToDelete: Record<string, unknown>
const { joinAliases, joins, selectFields, where } = await buildQuery({

View File

@@ -3,11 +3,11 @@ import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { inArray } from 'drizzle-orm'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const deleteVersions: DeleteVersions = async function deleteVersion(
this: PostgresAdapter,
@@ -16,10 +16,11 @@ export const deleteVersions: DeleteVersions = async function deleteVersion(
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = this.tableNameMap.get(
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
)
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
const { docs } = await findMany({

View File

@@ -1,11 +1,10 @@
import type { Find } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const find: Find = async function find(
this: PostgresAdapter,
@@ -22,8 +21,10 @@ export const find: Find = async function find(
) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
const tableName = getTableName({
adapter: this,
config: collectionConfig,
})
return findMany({
adapter: this,

View File

@@ -120,7 +120,7 @@ export const findMany = async function find({
const findPromise = db.query[tableName].findMany(findManyArgs)
if (pagination !== false && (orderedIDs ? orderedIDs?.length <= limit : true)) {
if (pagination !== false && (orderedIDs ? orderedIDs?.length >= limit : true)) {
const selectCountMethods: ChainedMethods = []
joinAliases.forEach(({ condition, table }) => {

View File

@@ -2,11 +2,12 @@
import type { Field } from 'payload/types'
import { fieldAffectsData, tabHasName } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../types'
import type { Result } from './buildFindManyArgs'
import { getTableName } from '../schema/getTableName'
type TraverseFieldArgs = {
_locales: Record<string, unknown>
adapter: PostgresAdapter
@@ -78,11 +79,20 @@ export const traverseFields = ({
with: {},
}
const arrayTableName = adapter.tableNameMap.get(
`${currentTableName}_${path}${toSnakeCase(field.name)}`,
)
const arrayTableName = getTableName({
adapter,
config: field,
parentTableName: currentTableName,
prefix: `${currentTableName}_${path}`,
})
const arrayTableNameWithLocales = `${arrayTableName}${adapter.localesSuffix}`
const arrayTableNameWithLocales = getTableName({
adapter,
config: field,
locales: true,
parentTableName: currentTableName,
prefix: `${currentTableName}_${path}`,
})
if (adapter.tables[arrayTableNameWithLocales]) withArray.with._locales = _locales
currentArgs.with[`${path}${field.name}`] = withArray
@@ -132,13 +142,15 @@ export const traverseFields = ({
with: {},
}
const tableName = adapter.tableNameMap.get(
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
)
const tableName = getTableName({
adapter,
config: block,
parentTableName: topLevelTableName,
prefix: `${topLevelTableName}_blocks_`,
})
if (adapter.tables[`${tableName}${adapter.localesSuffix}`]) {
if (adapter.tables[`${tableName}${adapter.localesSuffix}`])
withBlock.with._locales = _locales
}
topLevelArgs.with[blockKey] = withBlock
traverseFields({

View File

@@ -1,18 +1,19 @@
import type { FindGlobal } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const findGlobal: FindGlobal = async function findGlobal(
this: PostgresAdapter,
{ slug, locale, req, where },
) {
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))
const tableName = getTableName({
adapter: this,
config: globalConfig,
})
const {
docs: [doc],

View File

@@ -2,11 +2,11 @@ import type { FindGlobalVersions } from 'payload/database'
import type { PayloadRequest, SanitizedGlobalConfig } from 'payload/types'
import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
this: PostgresAdapter,
@@ -27,10 +27,11 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
)
const sort = typeof sortArg === 'string' ? sortArg : '-createdAt'
const tableName = this.tableNameMap.get(
`_${toSnakeCase(globalConfig.slug)}${this.versionsSuffix}`,
)
const tableName = getTableName({
adapter: this,
config: globalConfig,
versions: true,
})
const fields = buildVersionGlobalFields(globalConfig)
return findMany({

View File

@@ -1,19 +1,20 @@
import type { FindOneArgs } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export async function findOne<T extends TypeWithID>(
this: PostgresAdapter,
{ collection, locale, req = {} as PayloadRequest, where }: FindOneArgs,
): Promise<T> {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
const tableName = getTableName({
adapter: this,
config: collectionConfig,
})
const { docs } = await findMany({
adapter: this,

View File

@@ -2,11 +2,11 @@ import type { FindVersions } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const findVersions: FindVersions = async function findVersions(
this: PostgresAdapter,
@@ -25,10 +25,11 @@ export const findVersions: FindVersions = async function findVersions(
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort
const tableName = this.tableNameMap.get(
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
)
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
return findMany({

View File

@@ -7,7 +7,6 @@ import { createDatabaseAdapter } from 'payload/database'
import type { Args, PostgresAdapter, PostgresAdapterResult } from './types'
import { connect } from './connect'
import { count } from './count'
import { create } from './create'
import { createGlobal } from './createGlobal'
import { createGlobalVersion } from './createGlobalVersion'
@@ -41,18 +40,18 @@ import { updateVersion } from './updateVersion'
export type { MigrateDownArgs, MigrateUpArgs } from './types'
export function postgresAdapter(args: Args): PostgresAdapterResult {
const postgresIDType = args.idType || 'serial'
const payloadIDType = postgresIDType === 'serial' ? 'number' : 'text'
function adapter({ payload }: { payload: Payload }) {
const migrationDir = findMigrationDir(args.migrationDir)
const idType = args.idType || 'serial'
return createDatabaseAdapter<PostgresAdapter>({
name: 'postgres',
// Postgres-specific
blockTableNames: {},
drizzle: undefined,
enums: {},
fieldConstraints: {},
idType: postgresIDType,
idType,
localesSuffix: args.localesSuffix || '_locales',
logger: args.logger,
pgSchema: undefined,
@@ -64,7 +63,6 @@ export function postgresAdapter(args: Args): PostgresAdapterResult {
schema: {},
schemaName: args.schemaName,
sessions: {},
tableNameMap: new Map<string, string>(),
tables: {},
versionsSuffix: args.versionsSuffix || '_v',
@@ -72,13 +70,15 @@ export function postgresAdapter(args: Args): PostgresAdapterResult {
beginTransaction,
commitTransaction,
connect,
count,
create,
createGlobal,
createGlobalVersion,
createMigration,
createVersion,
defaultIDType: payloadIDType,
/**
* This represents how a default ID is treated in Payload as were a field type
*/
defaultIDType: idType === 'serial' ? 'number' : 'text',
deleteMany,
deleteOne,
deleteVersions,

View File

@@ -8,7 +8,7 @@ import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload/
import type { PostgresAdapter } from './types'
import { buildTable } from './schema/build'
import { createTableName } from './schema/createTableName'
import { getTableName } from './schema/getTableName'
export const init: Init = async function init(this: PostgresAdapter) {
if (this.schemaName) {
@@ -25,7 +25,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
}
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const tableName = createTableName({
const tableName = getTableName({
adapter: this,
config: collection,
})
@@ -44,11 +44,10 @@ export const init: Init = async function init(this: PostgresAdapter) {
})
if (collection.versions) {
const versionsTableName = createTableName({
const versionsTableName = getTableName({
adapter: this,
config: collection,
versions: true,
versionsCustomName: true,
})
const versionFields = buildVersionCollectionFields(collection)
@@ -68,7 +67,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
})
this.payload.config.globals.forEach((global) => {
const tableName = createTableName({ adapter: this, config: global })
const tableName = getTableName({ adapter: this, config: global })
buildTable({
adapter: this,
@@ -84,12 +83,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
})
if (global.versions) {
const versionsTableName = createTableName({
adapter: this,
config: global,
versions: true,
versionsCustomName: true,
})
const versionsTableName = getTableName({ adapter: this, config: global, versions: true })
const versionFields = buildVersionGlobalFields(global)
buildTable({

View File

@@ -14,6 +14,8 @@ import { v4 as uuid } from 'uuid'
import type { GenericColumn, GenericTable, PostgresAdapter } from '../types'
import type { BuildQueryJoinAliases, BuildQueryJoins } from './buildQuery'
import { getTableName } from '../schema/getTableName'
type Constraint = {
columnName: string
table: GenericTable | PgTableWithColumns<any>
@@ -183,7 +185,13 @@ export const getTableColumnFromPath = ({
case 'group': {
if (locale && field.localized && adapter.payload.config.localization) {
newTableName = `${tableName}${adapter.localesSuffix}`
newTableName = getTableName({
adapter,
config: field,
locales: true,
parentTableName: tableName,
prefix: `${tableName}_`,
})
joins[tableName] = eq(
adapter.tables[tableName].id,
@@ -217,87 +225,13 @@ export const getTableColumnFromPath = ({
})
}
case 'select': {
if (field.hasMany) {
const newTableName = adapter.tableNameMap.get(
`${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`,
)
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
eq(adapter.tables[tableName].id, adapter.tables[newTableName].parent),
eq(adapter.tables[newTableName]._locale, locale),
)
if (locale !== 'all') {
constraints.push({
columnName: '_locale',
table: adapter.tables[newTableName],
value: locale,
})
}
} else {
joins[newTableName] = eq(
adapter.tables[tableName].id,
adapter.tables[newTableName].parent,
)
}
return {
columnName: 'value',
constraints,
field,
table: adapter.tables[newTableName],
}
}
break
}
case 'text':
case 'number': {
if (field.hasMany) {
let tableType = 'texts'
let columnName = 'text'
if (field.type === 'number') {
tableType = 'numbers'
columnName = 'number'
}
newTableName = `${tableName}_${tableType}`
const joinConstraints = [
eq(adapter.tables[tableName].id, adapter.tables[newTableName].parent),
eq(adapter.tables[newTableName].path, `${constraintPath}${field.name}`),
]
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
...joinConstraints,
eq(adapter.tables[newTableName]._locale, locale),
)
if (locale !== 'all') {
constraints.push({
columnName: 'locale',
table: adapter.tables[newTableName],
value: locale,
})
}
} else {
joins[newTableName] = and(...joinConstraints)
}
return {
columnName,
constraints,
field,
table: adapter.tables[newTableName],
}
}
break
}
case 'array': {
newTableName = adapter.tableNameMap.get(
`${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`,
)
newTableName = getTableName({
adapter,
config: field,
parentTableName: `${tableName}_${tableNameSuffix}`,
prefix: `${tableName}_${tableNameSuffix}`,
})
constraintPath = `${constraintPath}${field.name}.%.`
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
@@ -344,11 +278,12 @@ export const getTableColumnFromPath = ({
const blockTypes = Array.isArray(value) ? value : [value]
blockTypes.forEach((blockType) => {
const block = field.blocks.find((block) => block.slug === blockType)
newTableName = adapter.tableNameMap.get(
`${tableName}_blocks_${toSnakeCase(block.slug)}`,
)
newTableName = getTableName({
adapter,
config: block,
parentTableName: tableName,
prefix: `${tableName}_blocks_`,
})
joins[newTableName] = eq(
adapter.tables[tableName].id,
adapter.tables[newTableName]._parentID,
@@ -368,9 +303,13 @@ export const getTableColumnFromPath = ({
}
const hasBlockField = field.blocks.some((block) => {
newTableName = adapter.tableNameMap.get(`${tableName}_blocks_${toSnakeCase(block.slug)}`)
newTableName = getTableName({
adapter,
config: block,
parentTableName: tableName,
prefix: `${tableName}_blocks_`,
})
constraintPath = `${constraintPath}${field.name}.%.`
let result
const blockConstraints = []
const blockSelectFields = {}
@@ -443,7 +382,6 @@ export const getTableColumnFromPath = ({
aliasRelationshipTableName,
)
// Join in the relationships table
if (locale && field.localized && adapter.payload.config.localization) {
joinAliases.push({
condition: and(
@@ -477,9 +415,10 @@ export const getTableColumnFromPath = ({
if (typeof field.relationTo === 'string') {
const relationshipConfig = adapter.payload.collections[field.relationTo].config
newTableName = adapter.tableNameMap.get(toSnakeCase(relationshipConfig.slug))
newTableName = getTableName({
adapter,
config: relationshipConfig,
})
// parent to relationship join table
relationshipFields = relationshipConfig.fields
@@ -499,13 +438,13 @@ export const getTableColumnFromPath = ({
}
}
} else if (newCollectionPath === 'value') {
const tableColumnsNames = field.relationTo.map((relationTo) => {
const relationTableName = adapter.tableNameMap.get(
toSnakeCase(adapter.payload.collections[relationTo].config.slug),
)
return `"${aliasRelationshipTableName}"."${relationTableName}_id"`
})
const tableColumnsNames = field.relationTo.map(
(relationTo) =>
`"${aliasRelationshipTableName}"."${getTableName({
adapter,
config: adapter.payload.collections[relationTo].config,
})}_id"`,
)
return {
constraints,
field,
@@ -545,41 +484,43 @@ export const getTableColumnFromPath = ({
value,
})
}
}
if (fieldAffectsData(field)) {
if (field.localized && adapter.payload.config.localization) {
// If localized, we go to localized table and set aliasTable to undefined
// so it is not picked up below to be used as targetTable
newTableName = `${tableName}${adapter.localesSuffix}`
default: {
if (fieldAffectsData(field)) {
if (field.localized && adapter.payload.config.localization) {
// If localized, we go to localized table and set aliasTable to undefined
// so it is not picked up below to be used as targetTable
newTableName = `${tableName}${adapter.localesSuffix}`
const parentTable = aliasTable || adapter.tables[tableName]
const parentTable = aliasTable || adapter.tables[tableName]
joins[newTableName] = eq(parentTable.id, adapter.tables[newTableName]._parentID)
joins[newTableName] = eq(parentTable.id, adapter.tables[newTableName]._parentID)
aliasTable = undefined
aliasTable = undefined
if (locale !== 'all') {
constraints.push({
columnName: '_locale',
table: adapter.tables[newTableName],
value: locale,
})
if (locale !== 'all') {
constraints.push({
columnName: '_locale',
table: adapter.tables[newTableName],
value: locale,
})
}
}
const targetTable = aliasTable || adapter.tables[newTableName]
selectFields[`${newTableName}.${columnPrefix}${field.name}`] =
targetTable[`${columnPrefix}${field.name}`]
return {
columnName: `${columnPrefix}${field.name}`,
constraints,
field,
pathSegments,
table: targetTable,
}
}
}
const targetTable = aliasTable || adapter.tables[newTableName]
selectFields[`${newTableName}.${columnPrefix}${field.name}`] =
targetTable[`${columnPrefix}${field.name}`]
return {
columnName: `${columnPrefix}${field.name}`,
constraints,
field,
pathSegments,
table: targetTable,
}
}
}

View File

@@ -10,8 +10,8 @@ import type { GenericColumn, PostgresAdapter } from '../types'
import type { BuildQueryJoinAliases, BuildQueryJoins } from './buildQuery'
import { buildAndOrConditions } from './buildAndOrConditions'
import { convertPathToJSONTraversal } from './createJSONQuery/convertPathToJSONTraversal'
import { createJSONQuery } from './createJSONQuery'
import { convertPathToJSONTraversal } from './createJSONQuery/convertPathToJSONTraversal'
import { getTableColumnFromPath } from './getTableColumnFromPath'
import { operatorMap } from './operatorMap'
import { sanitizeQueryValue } from './sanitizeQueryValue'

View File

@@ -85,10 +85,6 @@ export const sanitizeQueryValue = ({
}
}
if ('hasMany' in field && field.hasMany && operator === 'contains') {
operator = 'equals'
}
if (operator === 'near' || operator === 'within' || operator === 'intersects') {
throw new APIError(
`Querying with '${operator}' is not supported with the postgres database adapter.`,

View File

@@ -2,20 +2,26 @@ import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { type QueryDrafts, combineQueries } from 'payload/database'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const queryDrafts: QueryDrafts = async function queryDrafts(
this: PostgresAdapter,
{ collection, limit, locale, page = 1, pagination, req = {} as PayloadRequest, sort, where },
) {
export const queryDrafts: QueryDrafts = async function queryDrafts({
collection,
limit,
locale,
page = 1,
pagination,
req = {} as PayloadRequest,
sort,
where,
}) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = this.tableNameMap.get(
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
)
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
const combinedWhere = combineQueries({ latest: { equals: true } }, where)

View File

@@ -1,43 +1,28 @@
/* eslint-disable no-param-reassign */
import type { Relation } from 'drizzle-orm'
import type {
ForeignKeyBuilder,
IndexBuilder,
PgColumnBuilder,
PgTableWithColumns,
UniqueConstraintBuilder,
} from 'drizzle-orm/pg-core'
import { Field, fieldAffectsData } from 'payload/types'
import type { Field } from 'payload/types'
import { relations } from 'drizzle-orm'
import {
foreignKey,
index,
integer,
numeric,
serial,
timestamp,
unique,
varchar,
} from 'drizzle-orm/pg-core'
import toSnakeCase from 'to-snake-case'
import { index, integer, numeric, serial, timestamp, unique, varchar } from 'drizzle-orm/pg-core'
import { fieldAffectsData } from 'payload/types'
import type { GenericColumns, GenericTable, IDType, PostgresAdapter } from '../types'
import { createTableName } from './createTableName'
import { getTableName } from './getTableName'
import { parentIDColumnMap } from './parentIDColumnMap'
import { setColumnID } from './setColumnID'
import { traverseFields } from './traverseFields'
export type BaseExtraConfig = Record<
string,
(cols: GenericColumns) => ForeignKeyBuilder | IndexBuilder | UniqueConstraintBuilder
>
type Args = {
adapter: PostgresAdapter
baseColumns?: Record<string, PgColumnBuilder>
baseExtraConfig?: BaseExtraConfig
baseExtraConfig?: Record<string, (cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder>
buildNumbers?: boolean
buildRelationships?: boolean
buildTexts?: boolean
@@ -81,6 +66,13 @@ export const buildTable = ({
const columns: Record<string, PgColumnBuilder> = baseColumns
const indexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
let hasLocalizedField = false
let hasLocalizedRelationshipField = false
let hasManyTextField: 'index' | boolean = false
let hasManyNumberField: 'index' | boolean = false
let hasLocalizedManyTextField = false
let hasLocalizedManyNumberField = false
const localesColumns: Record<string, PgColumnBuilder> = {}
const localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
let localesTable: GenericTable | PgTableWithColumns<any>
@@ -97,7 +89,7 @@ export const buildTable = ({
const idColType: IDType = setColumnID({ adapter, columns, fields })
const {
;({
hasLocalizedField,
hasLocalizedManyNumberField,
hasLocalizedManyTextField,
@@ -124,7 +116,7 @@ export const buildTable = ({
rootTableIDColType: rootTableIDColType || idColType,
rootTableName,
versions,
})
}))
if (timestamps) {
columns.createdAt = timestamp('created_at', {
@@ -149,12 +141,10 @@ export const buildTable = ({
return config
}, {})
const result = Object.entries(indexes).reduce((acc, [colName, func]) => {
return Object.entries(indexes).reduce((acc, [colName, func]) => {
acc[colName] = func(cols)
return acc
}, extraConfig)
return result
})
adapter.tables[tableName] = table
@@ -163,7 +153,9 @@ export const buildTable = ({
const localeTableName = `${tableName}${adapter.localesSuffix}`
localesColumns.id = serial('id').primaryKey()
localesColumns._locale = adapter.enums.enum__locales('_locale').notNull()
localesColumns._parentID = parentIDColumnMap[idColType]('_parent_id').notNull()
localesColumns._parentID = parentIDColumnMap[idColType]('_parent_id')
.references(() => table.id, { onDelete: 'cascade' })
.notNull()
localesTable = adapter.pgSchema.table(localeTableName, localesColumns, (cols) => {
return Object.entries(localesIndexes).reduce(
@@ -176,11 +168,6 @@ export const buildTable = ({
cols._locale,
cols._parentID,
),
_parentIdFk: foreignKey({
name: `${localeTableName}_parent_id_fk`,
columns: [cols._parentID],
foreignColumns: [table.id],
}).onDelete('cascade'),
},
)
})
@@ -202,7 +189,9 @@ export const buildTable = ({
const columns: Record<string, PgColumnBuilder> = {
id: serial('id').primaryKey(),
order: integer('order').notNull(),
parent: parentIDColumnMap[idColType]('parent_id').notNull(),
parent: parentIDColumnMap[idColType]('parent_id')
.references(() => table.id, { onDelete: 'cascade' })
.notNull(),
path: varchar('path').notNull(),
text: varchar('text'),
}
@@ -212,24 +201,19 @@ export const buildTable = ({
}
textsTable = adapter.pgSchema.table(textsTableName, columns, (cols) => {
const config: Record<string, ForeignKeyBuilder | IndexBuilder> = {
const indexes: Record<string, IndexBuilder> = {
orderParentIdx: index(`${textsTableName}_order_parent_idx`).on(cols.order, cols.parent),
parentFk: foreignKey({
name: `${textsTableName}_parent_fk`,
columns: [cols.parent],
foreignColumns: [table.id],
}).onDelete('cascade'),
}
if (hasManyTextField === 'index') {
config.text_idx = index(`${textsTableName}_text_idx`).on(cols.text)
indexes.text_idx = index(`${textsTableName}_text_idx`).on(cols.text)
}
if (hasLocalizedManyTextField) {
config.localeParent = index(`${textsTableName}_locale_parent`).on(cols.locale, cols.parent)
indexes.localeParent = index(`${textsTableName}_locale_parent`).on(cols.locale, cols.parent)
}
return config
return indexes
})
adapter.tables[textsTableName] = textsTable
@@ -250,7 +234,9 @@ export const buildTable = ({
id: serial('id').primaryKey(),
number: numeric('number'),
order: integer('order').notNull(),
parent: parentIDColumnMap[idColType]('parent_id').notNull(),
parent: parentIDColumnMap[idColType]('parent_id')
.references(() => table.id, { onDelete: 'cascade' })
.notNull(),
path: varchar('path').notNull(),
}
@@ -259,27 +245,22 @@ export const buildTable = ({
}
numbersTable = adapter.pgSchema.table(numbersTableName, columns, (cols) => {
const config: Record<string, ForeignKeyBuilder | IndexBuilder> = {
const indexes: Record<string, IndexBuilder> = {
orderParentIdx: index(`${numbersTableName}_order_parent_idx`).on(cols.order, cols.parent),
parentFk: foreignKey({
name: `${numbersTableName}_parent_fk`,
columns: [cols.parent],
foreignColumns: [table.id],
}).onDelete('cascade'),
}
if (hasManyNumberField === 'index') {
config.numberIdx = index(`${numbersTableName}_number_idx`).on(cols.number)
indexes.numberIdx = index(`${numbersTableName}_number_idx`).on(cols.number)
}
if (hasLocalizedManyNumberField) {
config.localeParent = index(`${numbersTableName}_locale_parent`).on(
indexes.localeParent = index(`${numbersTableName}_locale_parent`).on(
cols.locale,
cols.parent,
)
}
return config
return indexes
})
adapter.tables[numbersTableName] = numbersTable
@@ -299,7 +280,9 @@ export const buildTable = ({
const relationshipColumns: Record<string, PgColumnBuilder> = {
id: serial('id').primaryKey(),
order: integer('order'),
parent: parentIDColumnMap[idColType]('parent_id').notNull(),
parent: parentIDColumnMap[idColType]('parent_id')
.references(() => table.id, { onDelete: 'cascade' })
.notNull(),
path: varchar('path').notNull(),
}
@@ -307,61 +290,36 @@ export const buildTable = ({
relationshipColumns.locale = adapter.enums.enum__locales('locale')
}
const relationExtraConfig: BaseExtraConfig = {}
const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`
relationships.forEach((relationTo) => {
const relationshipConfig = adapter.payload.collections[relationTo].config
const formattedRelationTo = createTableName({
const formattedRelationTo = getTableName({
adapter,
config: relationshipConfig,
throwValidationError: true,
})
let colType = adapter.idType === 'uuid' ? 'uuid' : 'integer'
const relatedCollectionCustomID = relationshipConfig.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
const relatedCollectionCustomIDType = relatedCollectionCustomID?.type
if (relatedCollectionCustomIDType === 'number') colType = 'numeric'
if (relatedCollectionCustomIDType === 'text') colType = 'varchar'
if (relatedCollectionCustomID?.type === 'number') colType = 'numeric'
if (relatedCollectionCustomID?.type === 'text') colType = 'varchar'
relationshipColumns[`${relationTo}ID`] = parentIDColumnMap[colType](
`${formattedRelationTo}_id`,
)
relationExtraConfig[`${relationTo}IdFk`] = (cols) =>
foreignKey({
name: `${relationshipsTableName}_${toSnakeCase(relationTo)}_fk`,
columns: [cols[`${relationTo}ID`]],
foreignColumns: [adapter.tables[formattedRelationTo].id],
}).onDelete('cascade')
).references(() => adapter.tables[formattedRelationTo].id, { onDelete: 'cascade' })
})
const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`
relationshipsTable = adapter.pgSchema.table(
relationshipsTableName,
relationshipColumns,
(cols) => {
const result: Record<string, ForeignKeyBuilder | IndexBuilder> = Object.entries(
relationExtraConfig,
).reduce(
(config, [key, func]) => {
config[key] = func(cols)
return config
},
{
order: index(`${relationshipsTableName}_order_idx`).on(cols.order),
parentFk: foreignKey({
name: `${relationshipsTableName}_parent_fk`,
columns: [cols.parent],
foreignColumns: [table.id],
}).onDelete('cascade'),
parentIdx: index(`${relationshipsTableName}_parent_idx`).on(cols.parent),
pathIdx: index(`${relationshipsTableName}_path_idx`).on(cols.path),
},
)
const result: Record<string, unknown> = {
order: index(`${relationshipsTableName}_order_idx`).on(cols.order),
parentIdx: index(`${relationshipsTableName}_parent_idx`).on(cols.parent),
pathIdx: index(`${relationshipsTableName}_path_idx`).on(cols.path),
}
if (hasLocalizedRelationshipField) {
result.localeIdx = index(`${relationshipsTableName}_locale_idx`).on(cols.locale)
@@ -383,7 +341,7 @@ export const buildTable = ({
}
relationships.forEach((relationTo) => {
const relatedTableName = createTableName({
const relatedTableName = getTableName({
adapter,
config: adapter.payload.collections[relationTo].config,
throwValidationError: true,

View File

@@ -14,59 +14,53 @@ type Args = {
name?: string
slug?: string
}
/** Localized tables need to be given the locales suffix */
locales?: boolean
/** For nested tables passed for the user custom dbName functions to handle their own iterations */
parentTableName?: string
/** For sub tables (array for example) this needs to include the parentTableName */
prefix?: string
/** Adds the relationships suffix */
relationships?: boolean
/** For tables based on fields that could have both enumName and dbName (ie: select with hasMany), default: 'dbName' */
target?: 'dbName' | 'enumName'
throwValidationError?: boolean
/** Adds the versions suffix to the default table name - should only be used on the base collection to avoid duplicate suffixing */
/** Adds the versions suffix, should only be used on the base collection to duplicate suffixing */
versions?: boolean
/** Adds the versions suffix to custom dbName only - this is used while creating blocks / selects / arrays / etc */
versionsCustomName?: boolean
}
/**
* Used to name database enums and tables
* Returns the table or enum name for a given entity
*/
export const createTableName = ({
export const getTableName = ({
adapter,
config: { name, slug },
config,
locales = false,
parentTableName,
prefix = '',
relationships = false,
target = 'dbName',
throwValidationError = false,
versions = false,
versionsCustomName = false,
}: Args): string => {
let customNameDefinition = config[target]
let result: string
let custom = config[target]
let defaultTableName = `${prefix}${toSnakeCase(name ?? slug)}`
if (versions) defaultTableName = `_${defaultTableName}${adapter.versionsSuffix}`
let customTableNameResult: string
if (!customNameDefinition && target === 'enumName') {
customNameDefinition = config['dbName']
if (!custom && target === 'enumName') {
custom = config['dbName']
}
if (customNameDefinition) {
customTableNameResult =
typeof customNameDefinition === 'function'
? customNameDefinition({ tableName: parentTableName })
: customNameDefinition
if (versionsCustomName)
customTableNameResult = `_${customTableNameResult}${adapter.versionsSuffix}`
if (custom) {
result = typeof custom === 'function' ? custom({ tableName: parentTableName }) : custom
} else {
result = `${prefix}${toSnakeCase(name ?? slug)}`
}
const result = customTableNameResult || defaultTableName
adapter.tableNameMap.set(defaultTableName, result)
if (locales) result = `${result}${adapter.localesSuffix}`
if (versions) result = `_${result}${adapter.versionsSuffix}`
if (relationships) result = `${result}${adapter.relationshipsSuffix}`
if (!throwValidationError) {
return result
@@ -77,6 +71,5 @@ export const createTableName = ({
`Exceeded max identifier length for table or enum name of 63 characters. Invalid name: ${result}`,
)
}
return result
}

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */
import type { Relation } from 'drizzle-orm'
import type { IndexBuilder, PgColumnBuilder } from 'drizzle-orm/pg-core'
import type { IndexBuilder, PgColumnBuilder, UniqueConstraintBuilder } from 'drizzle-orm/pg-core'
import type { Field, TabAsField } from 'payload/types'
import { relations } from 'drizzle-orm'
@@ -9,7 +9,6 @@ import {
PgUUIDBuilder,
PgVarcharBuilder,
boolean,
foreignKey,
index,
integer,
jsonb,
@@ -24,12 +23,11 @@ import { fieldAffectsData, optionIsObject } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { GenericColumns, IDType, PostgresAdapter } from '../types'
import type { BaseExtraConfig } from './build'
import { hasLocalesTable } from '../utilities/hasLocalesTable'
import { buildTable } from './build'
import { createIndex } from './createIndex'
import { createTableName } from './createTableName'
import { getTableName } from './getTableName'
import { idToUUID } from './idToUUID'
import { parentIDColumnMap } from './parentIDColumnMap'
import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdentical'
@@ -223,13 +221,14 @@ export const traverseFields = ({
case 'radio':
case 'select': {
const enumName = createTableName({
const enumName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `enum_${newTableName}_`,
target: 'enumName',
throwValidationError,
versions,
})
adapter.enums[enumName] = pgEnum(
@@ -244,27 +243,27 @@ export const traverseFields = ({
)
if (field.type === 'select' && field.hasMany) {
const selectTableName = createTableName({
const selectTableName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
versions,
})
const baseColumns: Record<string, PgColumnBuilder> = {
order: integer('order').notNull(),
parent: parentIDColumnMap[parentIDColType]('parent_id').notNull(),
parent: parentIDColumnMap[parentIDColType]('parent_id')
.references(() => adapter.tables[parentTableName].id, { onDelete: 'cascade' })
.notNull(),
value: adapter.enums[enumName]('value'),
}
const baseExtraConfig: BaseExtraConfig = {
const baseExtraConfig: Record<
string,
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
> = {
orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order),
parentFk: (cols) =>
foreignKey({
name: `${selectTableName}_parent_fk`,
columns: [cols.parent],
foreignColumns: [adapter.tables[parentTableName].id],
}),
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
}
@@ -317,28 +316,25 @@ export const traverseFields = ({
case 'array': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const arrayTableName = createTableName({
const arrayTableName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
versionsCustomName: versions,
})
const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
_parentID: parentIDColumnMap[parentIDColType]('_parent_id').notNull(),
_parentID: parentIDColumnMap[parentIDColType]('_parent_id')
.references(() => adapter.tables[parentTableName].id, { onDelete: 'cascade' })
.notNull(),
}
const baseExtraConfig: BaseExtraConfig = {
const baseExtraConfig: Record<
string,
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
> = {
_orderIdx: (cols) => index(`${arrayTableName}_order_idx`).on(cols._order),
_parentIDFk: (cols) =>
foreignKey({
name: `${arrayTableName}_parent_id_fk`,
columns: [cols['_parentID']],
foreignColumns: [adapter.tables[parentTableName].id],
}).onDelete('cascade'),
_parentIDIdx: (cols) => index(`${arrayTableName}_parent_id_idx`).on(cols._parentID),
}
@@ -406,30 +402,28 @@ export const traverseFields = ({
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
field.blocks.forEach((block) => {
const blockTableName = createTableName({
const blockTableName = getTableName({
adapter,
config: block,
parentTableName: rootTableName,
prefix: `${rootTableName}_blocks_`,
throwValidationError,
versionsCustomName: versions,
})
if (!adapter.tables[blockTableName]) {
const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
_parentID: parentIDColumnMap[rootTableIDColType]('_parent_id').notNull(),
_parentID: parentIDColumnMap[rootTableIDColType]('_parent_id')
.references(() => adapter.tables[rootTableName].id, { onDelete: 'cascade' })
.notNull(),
_path: text('_path').notNull(),
}
const baseExtraConfig: BaseExtraConfig = {
const baseExtraConfig: Record<
string,
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
> = {
_orderIdx: (cols) => index(`${blockTableName}_order_idx`).on(cols._order),
_parentIDIdx: (cols) => index(`${blockTableName}_parent_id_idx`).on(cols._parentID),
_parentIdFk: (cols) =>
foreignKey({
name: `${blockTableName}_parent_id_fk`,
columns: [cols._parentID],
foreignColumns: [adapter.tables[rootTableName].id],
}).onDelete('cascade'),
_pathIdx: (cols) => index(`${blockTableName}_path_idx`).on(cols._path),
}
@@ -499,9 +493,9 @@ export const traverseFields = ({
localized: field.localized,
rootTableName,
table: adapter.tables[blockTableName],
tableLocales: adapter.tables[`${blockTableName}${adapter.localesSuffix}`],
})
}
adapter.blockTableNames[`${rootTableName}.${toSnakeCase(block.slug)}`] = blockTableName
rootRelationsToBuild.set(`_blocks_${block.slug}`, blockTableName)
})
@@ -664,7 +658,7 @@ export const traverseFields = ({
indexes,
localesColumns,
localesIndexes,
newTableName,
newTableName: parentTableName,
parentTableName,
relationsToBuild,
relationships,

View File

@@ -10,13 +10,9 @@ type Args = {
localized: boolean
rootTableName: string
table: GenericTable
tableLocales?: GenericTable
}
const getFlattenedFieldNames = (
fields: Field[],
prefix: string = '',
): { localized?: boolean; name: string }[] => {
const getFlattenedFieldNames = (fields: Field[], prefix: string = ''): string[] => {
return fields.reduce((fieldsToUse, field) => {
let fieldPrefix = prefix
@@ -28,7 +24,7 @@ const getFlattenedFieldNames = (
}
if (fieldHasSubFields(field)) {
fieldPrefix = 'name' in field ? `${prefix}${field.name}_` : prefix
fieldPrefix = 'name' in field ? `${prefix}${field.name}.` : prefix
return [...fieldsToUse, ...getFlattenedFieldNames(field.fields, fieldPrefix)]
}
@@ -36,7 +32,7 @@ const getFlattenedFieldNames = (
return [
...fieldsToUse,
...field.tabs.reduce((tabFields, tab) => {
fieldPrefix = 'name' in tab ? `${prefix}_${tab.name}` : prefix
fieldPrefix = 'name' in tab ? `${prefix}.${tab.name}` : prefix
return [
...tabFields,
...(tabHasName(tab)
@@ -48,13 +44,7 @@ const getFlattenedFieldNames = (
}
if (fieldAffectsData(field)) {
return [
...fieldsToUse,
{
name: `${fieldPrefix}${field.name}`,
localized: field.localized,
},
]
return [...fieldsToUse, `${fieldPrefix?.replace('.', '_') || ''}${field.name}`]
}
return fieldsToUse
@@ -66,30 +56,22 @@ export const validateExistingBlockIsIdentical = ({
localized,
rootTableName,
table,
tableLocales,
}: Args): void => {
const fieldNames = getFlattenedFieldNames(block.fields)
const missingField =
// ensure every field from the config is in the matching table
fieldNames.find(({ name, localized }) => {
const fieldTable = localized && tableLocales ? tableLocales : table
return Object.keys(fieldTable).indexOf(name) === -1
}) ||
fieldNames.find((name) => Object.keys(table).indexOf(name) === -1) ||
// ensure every table column is matched for every field from the config
Object.keys(table).find((fieldName) => {
if (!['_locale', '_order', '_parentID', '_path', '_uuid'].includes(fieldName)) {
return fieldNames.findIndex((field) => field.name) === -1
return fieldNames.indexOf(fieldName) === -1
}
})
if (missingField) {
throw new InvalidConfiguration(
`The table ${rootTableName} has multiple blocks with slug ${
block.slug
}, but the schemas do not match. One block includes the field ${
typeof missingField === 'string' ? missingField : missingField.name
}, while the other block does not.`,
`The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One block includes the field ${missingField}, while the other block does not.`,
)
}

View File

@@ -4,11 +4,11 @@ import type { TextField } from 'payload/types'
type Args = {
field: TextField
locale?: string
ref: Record<string, unknown>
textRows: Record<string, unknown>[]
ref: Record<string, unknown>
}
export const transformHasManyText = ({ field, locale, ref, textRows }: Args) => {
export const transformHasManyText = ({ field, locale, textRows, ref }: Args) => {
const result = textRows.map(({ text }) => text)
if (locale) {

View File

@@ -48,11 +48,11 @@ export const transform = <T extends TypeWithID>({ config, data, fields }: Transf
deletions,
fieldPrefix: '',
fields,
texts,
numbers,
path: '',
relationships,
table: data,
texts,
})
deletions.forEach((deletion) => deletion())

View File

@@ -18,6 +18,7 @@ type Args = {
data: unknown
field: ArrayField
locale?: string
texts: Record<string, unknown>[]
numbers: Record<string, unknown>[]
path: string
relationships: Record<string, unknown>[]
@@ -25,7 +26,6 @@ type Args = {
selects: {
[tableName: string]: Record<string, unknown>[]
}
texts: Record<string, unknown>[]
}
export const transformArray = ({
@@ -37,15 +37,14 @@ export const transformArray = ({
data,
field,
locale,
texts,
numbers,
path,
relationships,
relationshipsToDelete,
selects,
texts,
}: Args) => {
const newRows: ArrayRowToInsert[] = []
const hasUUID = adapter.tables[arrayTableName]._uuid
if (isArrayOfRows(data)) {
@@ -89,6 +88,7 @@ export const transformArray = ({
fieldPrefix: '',
fields: field.fields,
locales: newRow.locales,
texts,
numbers,
parentTableName: arrayTableName,
path: `${path || ''}${field.name}.${i}.`,
@@ -96,7 +96,6 @@ export const transformArray = ({
relationshipsToDelete,
row: newRow.row,
selects,
texts,
})
newRows.push(newRow)

View File

@@ -61,7 +61,7 @@ export const transformBlocks = ({
if (field.localized && locale) newRow.row._locale = locale
const blockTableName = adapter.tableNameMap.get(`${baseTableName}_blocks_${blockType}`)
const blockTableName = `${baseTableName}_blocks_${blockType}`
const hasUUID = adapter.tables[blockTableName]._uuid

View File

@@ -27,12 +27,12 @@ export const transformForWrite = ({
blocks: {},
blocksToDelete: new Set(),
locales: {},
texts: [],
numbers: [],
relationships: [],
relationshipsToDelete: [],
row: {},
selects: {},
texts: [],
}
// This function is responsible for building up the
@@ -48,6 +48,7 @@ export const transformForWrite = ({
fieldPrefix: '',
fields,
locales: rowToInsert.locales,
texts: rowToInsert.texts,
numbers: rowToInsert.numbers,
parentTableName: tableName,
path,
@@ -55,7 +56,6 @@ export const transformForWrite = ({
relationshipsToDelete: rowToInsert.relationshipsToDelete,
row: rowToInsert.row,
selects: rowToInsert.selects,
texts: rowToInsert.texts,
})
return rowToInsert

View File

@@ -8,8 +8,8 @@ export const transformTexts = ({ baseRow, data, texts }: Args) => {
data.forEach((val, i) => {
texts.push({
...baseRow,
order: i + 1,
text: val,
order: i + 1,
})
})
}

View File

@@ -7,6 +7,7 @@ import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../../types'
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types'
import { getTableName } from '../../schema/getTableName'
import { isArrayOfRows } from '../../utilities/isArrayOfRows'
import { transformArray } from './array'
import { transformBlocks } from './blocks'
@@ -88,13 +89,18 @@ export const traverseFields = ({
let fieldData: unknown
if (fieldAffectsData(field)) {
columnName = `${columnPrefix || ''}${toSnakeCase(field.name)}`
columnName = `${columnPrefix || ''}${getTableName({
adapter,
config: field,
// do not pass columnPrefix here because it is required and custom dbNames also need it
prefix: '',
})}`
fieldName = `${fieldPrefix || ''}${field.name}`
fieldData = data[field.name]
}
if (field.type === 'array') {
const arrayTableName = adapter.tableNameMap.get(`${parentTableName}_${columnName}`)
const arrayTableName = `${parentTableName}_${columnName}`
if (!arrays[arrayTableName]) arrays[arrayTableName] = []
@@ -147,8 +153,8 @@ export const traverseFields = ({
}
if (field.type === 'blocks') {
field.blocks.forEach(({ slug }) => {
blocksToDelete.add(toSnakeCase(slug))
field.blocks.forEach((block) => {
blocksToDelete.add(getTableName({ adapter, config: block }))
})
if (field.localized) {
@@ -458,7 +464,7 @@ export const traverseFields = ({
}
if (field.type === 'select' && field.hasMany) {
const selectTableName = adapter.tableNameMap.get(`${parentTableName}_${columnName}`)
const selectTableName = `${parentTableName}_${columnName}`
if (!selects[selectTableName]) selects[selectTableName] = []
if (field.localized) {
@@ -488,7 +494,11 @@ export const traverseFields = ({
}
if (fieldAffectsData(field)) {
const valuesToTransform: { localeKey?: string; ref: unknown; value: unknown }[] = []
const valuesToTransform: {
localeKey?: string
ref: unknown
value: unknown
}[] = []
if (field.localized) {
if (typeof fieldData === 'object' && fieldData !== null) {

View File

@@ -34,6 +34,7 @@ export type RowToInsert = {
locales: {
[locale: string]: Record<string, unknown>
}
texts: Record<string, unknown>[]
numbers: Record<string, unknown>[]
relationships: Record<string, unknown>[]
relationshipsToDelete: RelationshipToDelete[]
@@ -41,5 +42,4 @@ export type RowToInsert = {
selects: {
[tableName: string]: Record<string, unknown>[]
}
texts: Record<string, unknown>[]
}

View File

@@ -23,14 +23,14 @@ import type { Pool, PoolConfig } from 'pg'
export type DrizzleDB = NodePgDatabase<Record<string, unknown>>
export type Args = {
idType?: 'serial' | 'uuid'
localesSuffix?: string
idType?: 'serial' | 'uuid'
logger?: DrizzleConfig['logger']
migrationDir?: string
pool: PoolConfig
push?: boolean
relationshipsSuffix?: string
schemaName?: string
relationshipsSuffix?: string
versionsSuffix?: string
}
@@ -61,6 +61,10 @@ export type DrizzleTransaction = PgTransaction<
>
export type PostgresAdapter = BaseDatabaseAdapter & {
/**
* Used internally to map the block name to the table name
*/
blockTableNames: Record<string, string>
drizzle: DrizzleDB
enums: Record<string, GenericEnum>
/**
@@ -86,7 +90,6 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
resolve: () => Promise<void>
}
}
tableNameMap: Map<string, string>
tables: Record<string, GenericTable | PgTableWithColumns<any>>
versionsSuffix?: string
}

View File

@@ -2,10 +2,12 @@ import type { UpdateOne } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { ChainedMethods } from './find/chainMethods'
import type { PostgresAdapter } from './types'
import buildQuery from './queries/buildQuery'
import { selectDistinct } from './queries/selectDistinct'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow'
export const updateOne: UpdateOne = async function updateOne(
@@ -14,7 +16,10 @@ export const updateOne: UpdateOne = async function updateOne(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
const tableName = getTableName({
adapter: this,
config: collection,
})
const whereToUse = whereArg || { id: { equals: id } }
let idToUpdate = id

View File

@@ -1,10 +1,9 @@
import type { UpdateGlobalArgs } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow'
export async function updateGlobal<T extends TypeWithID>(
@@ -13,7 +12,10 @@ export async function updateGlobal<T extends TypeWithID>(
): Promise<T> {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))
const tableName = getTableName({
adapter: this,
config: globalConfig,
})
const existingGlobal = await db.query[tableName].findFirst({})

View File

@@ -2,11 +2,11 @@ import type { TypeWithVersion, UpdateGlobalVersionArgs } from 'payload/database'
import type { PayloadRequest, SanitizedGlobalConfig, TypeWithID } from 'payload/types'
import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import buildQuery from './queries/buildQuery'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow'
export async function updateGlobalVersion<T extends TypeWithID>(
@@ -25,11 +25,11 @@ export async function updateGlobalVersion<T extends TypeWithID>(
({ slug }) => slug === global,
)
const whereToUse = whereArg || { id: { equals: id } }
const tableName = this.tableNameMap.get(
`_${toSnakeCase(globalConfig.slug)}${this.versionsSuffix}`,
)
const tableName = getTableName({
adapter: this,
config: globalConfig,
versions: true,
})
const fields = buildVersionGlobalFields(globalConfig)
const { where } = await buildQuery({

View File

@@ -2,11 +2,11 @@ import type { TypeWithVersion, UpdateVersionArgs } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig, TypeWithID } from 'payload/types'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import buildQuery from './queries/buildQuery'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow'
export async function updateVersion<T extends TypeWithID>(
@@ -23,10 +23,11 @@ export async function updateVersion<T extends TypeWithID>(
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const whereToUse = whereArg || { id: { equals: id } }
const tableName = this.tableNameMap.get(
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
)
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
const { where } = await buildQuery({

View File

@@ -224,14 +224,14 @@ export const upsertRow = async <T extends TypeWithID>({
if (operation === 'update') {
for (const blockName of rowToInsert.blocksToDelete) {
const blockTableName = adapter.tableNameMap.get(`${tableName}_blocks_${blockName}`)
const blockTableName = adapter.blockTableNames[`${tableName}.${blockName}`]
const blockTable = adapter.tables[blockTableName]
await db.delete(blockTable).where(eq(blockTable._parentID, insertedRow.id))
}
}
for (const [blockName, blockRows] of Object.entries(blocksToInsert)) {
const blockTableName = adapter.tableNameMap.get(`${tableName}_blocks_${blockName}`)
const blockTableName = adapter.blockTableNames[`${tableName}.${blockName}`]
insertedBlockRows[blockName] = await db
.insert(adapter.tables[blockTableName])
.values(blockRows.map(({ row }) => row))

View File

@@ -73,7 +73,7 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P
// Insert locale rows
if (adapter.tables[`${tableName}${adapter.localesSuffix}`] && row.locales.length > 0) {
if (!row.locales[0]._parentID) {
row.locales = row.locales.map((localeRow) => {
row.locales = row.locales.map((localeRow, i) => {
if (typeof localeRow._getParentID === 'function') {
localeRow._parentID = localeRow._getParentID(insertedRows)
delete localeRow._getParentID

View File

@@ -1,10 +0,0 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp

View File

@@ -1,37 +0,0 @@
/** @type {import('prettier').Config} */
module.exports = {
extends: ['@payloadcms'],
overrides: [
{
extends: ['plugin:@typescript-eslint/disable-type-checked'],
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
},
{
files: ['package.json', 'tsconfig.json'],
rules: {
'perfectionist/sort-array-includes': 'off',
'perfectionist/sort-astro-attributes': 'off',
'perfectionist/sort-classes': 'off',
'perfectionist/sort-enums': 'off',
'perfectionist/sort-exports': 'off',
'perfectionist/sort-imports': 'off',
'perfectionist/sort-interfaces': 'off',
'perfectionist/sort-jsx-props': 'off',
'perfectionist/sort-keys': 'off',
'perfectionist/sort-maps': 'off',
'perfectionist/sort-named-exports': 'off',
'perfectionist/sort-named-imports': 'off',
'perfectionist/sort-object-types': 'off',
'perfectionist/sort-objects': 'off',
'perfectionist/sort-svelte-attributes': 'off',
'perfectionist/sort-union-types': 'off',
'perfectionist/sort-vue-attributes': 'off',
},
},
],
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
root: true,
}

View File

@@ -1,10 +0,0 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp

View File

@@ -1,15 +0,0 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": "inline",
"jsc": {
"target": "esnext",
"parser": {
"syntax": "typescript",
"tsx": true,
"dts": true
}
},
"module": {
"type": "commonjs"
}
}

View File

@@ -1,49 +0,0 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "0.1.0",
"description": "The official live preview Vue SDK for Payload",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/live-preview-vue"
},
"license": "MIT",
"homepage": "https://payloadcms.com",
"author": "Payload CMS, Inc.",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"clean": "rimraf {dist,*.tsbuildinfo}",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
"prepublishOnly": "pnpm clean && pnpm build"
},
"dependencies": {
"@payloadcms/live-preview": "workspace:^0.x"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"vue": "^3.0.0",
"payload": "workspace:*"
},
"peerDependencies": {
"vue": "^3.0.0"
},
"exports": {
".": {
"default": "./src/index.ts",
"types": "./src/index.ts"
}
},
"publishConfig": {
"exports": null,
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"
},
"files": [
"dist"
]
}

View File

@@ -1,58 +0,0 @@
import type { Ref } from 'vue'
import { ready, subscribe, unsubscribe } from '@payloadcms/live-preview'
import { onMounted, onUnmounted, ref } from 'vue'
/**
* Vue composable to implement Payload CMS Live Preview.
*
* {@link https://payloadcms.com/docs/live-preview/frontend View the documentation}
*/
export const useLivePreview = <T>(props: {
apiRoute?: string
depth?: number
initialData: T
serverURL: string
}): {
data: Ref<T>
isLoading: Ref<boolean>
} => {
const { apiRoute, depth, initialData, serverURL } = props
const data = ref(initialData) as Ref<T>
const isLoading = ref(true)
const hasSentReadyMessage = ref(false)
const onChange = (mergedData: T) => {
data.value = mergedData
isLoading.value = false
}
let subscription: (event: MessageEvent) => void
onMounted(() => {
subscription = subscribe({
apiRoute,
callback: onChange,
depth,
initialData,
serverURL,
})
if (!hasSentReadyMessage.value) {
hasSentReadyMessage.value = true
ready({
serverURL,
})
}
})
onUnmounted(() => {
unsubscribe(subscription)
})
return {
data,
isLoading,
}
}

View File

@@ -1,25 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true, // Make sure typescript knows that this module depends on their references
"noEmit": false /* Do not emit outputs. */,
"emitDeclarationOnly": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
"jsx": "react"
},
"exclude": [
"dist",
"build",
"tests",
"test",
"node_modules",
".eslintrc.js",
"src/**/*.spec.js",
"src/**/*.spec.jsx",
"src/**/*.spec.ts",
"src/**/*.spec.tsx"
],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
"references": [{ "path": "../payload" }] // db-mongodb depends on payload
}

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "2.14.1",
"version": "2.12.1",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./dist/index.js",

View File

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

View File

@@ -75,7 +75,7 @@ export const DocumentControls: React.FC<{
label:
typeof collection?.labels?.singular === 'string'
? collection.labels.singular
: t('document'),
: 'document',
})}
</p>
</li>

View File

@@ -83,7 +83,7 @@ const SaveDraft: React.FC<{ action: string; disabled: boolean }> = ({ action, di
)
}
const EditMany: React.FC<Props> = (props) => {
const { collection: { slug, fields, labels: { plural } } = {}, collection, resetParams } = props
const { collection: { fields, labels: { plural }, slug } = {}, collection, resetParams } = props
const { permissions } = useAuth()
const { closeModal } = useModal()
@@ -148,11 +148,11 @@ const EditMany: React.FC<Props> = (props) => {
{collection.versions ? (
<React.Fragment>
<Publish
action={`${serverURL}${api}/${slug}${getQueryParams()}&draft=true`}
action={`${serverURL}${api}/${slug}${getQueryParams()}`}
disabled={selected.length === 0}
/>
<SaveDraft
action={`${serverURL}${api}/${slug}${getQueryParams()}&draft=true`}
action={`${serverURL}${api}/${slug}${getQueryParams()}`}
disabled={selected.length === 0}
/>
</React.Fragment>

View File

@@ -77,15 +77,6 @@ const contains = {
value: 'contains',
}
const filterOperators = (operators, hasMany = false) => {
if (hasMany) {
return operators.filter(
(operator) => operator.value !== 'equals' && operator.value !== 'not_equals',
)
}
return operators
}
const fieldTypeConditions = {
checkbox: {
component: 'Text',
@@ -109,7 +100,7 @@ const fieldTypeConditions = {
},
number: {
component: 'Number',
operators: (hasMany) => filterOperators([...base, ...numeric], hasMany),
operators: [...base, ...numeric],
},
point: {
component: 'Point',
@@ -129,11 +120,11 @@ const fieldTypeConditions = {
},
select: {
component: 'Select',
operators: (hasMany) => filterOperators([...base], hasMany),
operators: [...base],
},
text: {
component: 'Text',
operators: (hasMany) => filterOperators([...base, like, contains], hasMany),
operators: [...base, like, contains],
},
textarea: {
component: 'Text',

View File

@@ -22,44 +22,36 @@ const baseClass = 'where-builder'
const reduceFields = (fields, i18n) =>
flattenTopLevelFields(fields).reduce((reduced, field) => {
let operators = []
if (typeof fieldTypes[field.type] === 'object') {
if (typeof fieldTypes[field.type].operators === 'function') {
operators = fieldTypes[field.type].operators(
'hasMany' in field && field.hasMany ? true : false,
)
} else {
operators = fieldTypes[field.type].operators
const operatorKeys = new Set()
const operators = fieldTypes[field.type].operators.reduce((acc, operator) => {
if (!operatorKeys.has(operator.value)) {
operatorKeys.add(operator.value)
return [
...acc,
{
...operator,
label: i18n.t(`operators:${operator.label}`),
},
]
}
return acc
}, [])
const formattedField = {
label: getTranslation(field.label || field.name, i18n),
value: field.name,
...fieldTypes[field.type],
operators,
props: {
...field,
},
}
return [...reduced, formattedField]
}
const operatorKeys = new Set()
const filteredOperators = operators.reduce((acc, operator) => {
if (!operatorKeys.has(operator.value)) {
operatorKeys.add(operator.value)
return [
...acc,
{
...operator,
label: i18n.t(`operators:${operator.label}`),
},
]
}
return acc
}, [])
const formattedField = {
label: getTranslation(field.label || field.name, i18n),
value: field.name,
...fieldTypes[field.type],
operators: filteredOperators,
props: {
...field,
},
}
return [...reduced, formattedField]
return reduced
}, [])
/**
@@ -193,7 +185,7 @@ const WhereBuilder: React.FC<Props> = (props) => {
iconStyle="with-border"
onClick={() => {
if (reducedFields.length > 0)
dispatchConditions({ type: 'add', field: reducedFields[0].value })
dispatchConditions({ field: reducedFields[0].value, type: 'add' })
}}
>
{t('or')}
@@ -211,7 +203,7 @@ const WhereBuilder: React.FC<Props> = (props) => {
iconStyle="with-border"
onClick={() => {
if (reducedFields.length > 0)
dispatchConditions({ type: 'add', field: reducedFields[0].value })
dispatchConditions({ field: reducedFields[0].value, type: 'add' })
}}
>
{t('addFilter')}

View File

@@ -41,53 +41,46 @@ const CollapsibleField: React.FC<Props> = (props) => {
async (newCollapsedState: boolean) => {
const existingPreferences: DocumentPreferences = await getPreference(preferencesKey)
if (preferencesKey) {
await setPreference(preferencesKey, {
...existingPreferences,
...(path
? {
fields: {
...(existingPreferences?.fields || {}),
[path]: {
...existingPreferences?.fields?.[path],
collapsed: newCollapsedState,
},
setPreference(preferencesKey, {
...existingPreferences,
...(path
? {
fields: {
...(existingPreferences?.fields || {}),
[path]: {
...existingPreferences?.fields?.[path],
collapsed: newCollapsedState,
},
}
: {
fields: {
...(existingPreferences?.fields || {}),
[fieldPreferencesKey]: {
...existingPreferences?.fields?.[fieldPreferencesKey],
collapsed: newCollapsedState,
},
},
}
: {
fields: {
...(existingPreferences?.fields || {}),
[fieldPreferencesKey]: {
...existingPreferences?.fields?.[fieldPreferencesKey],
collapsed: newCollapsedState,
},
}),
})
}
},
}),
})
},
[preferencesKey, fieldPreferencesKey, getPreference, setPreference, path],
)
useEffect(() => {
const fetchInitialState = async () => {
if (preferencesKey) {
const preferences = await getPreference(preferencesKey)
const specificPreference = path
const preferences = await getPreference(preferencesKey)
if (preferences) {
const initCollapsedFromPref = path
? preferences?.fields?.[path]?.collapsed
: preferences?.fields?.[fieldPreferencesKey]?.collapsed
if (specificPreference !== undefined) {
setCollapsedOnMount(Boolean(specificPreference))
} else {
setCollapsedOnMount(typeof initCollapsed === 'boolean' ? initCollapsed : false)
}
setCollapsedOnMount(Boolean(initCollapsedFromPref))
} else {
setCollapsedOnMount(typeof initCollapsed === 'boolean' ? initCollapsed : false)
}
}
void fetchInitialState()
fetchInitialState()
}, [getPreference, preferencesKey, fieldPreferencesKey, initCollapsed, path])
if (typeof collapsedOnMount !== 'boolean') return null

View File

@@ -27,7 +27,6 @@ const JSONField: React.FC<Props> = (props) => {
style,
width,
} = {},
jsonSchema,
label,
path: pathFromProps,
required,
@@ -55,25 +54,6 @@ const JSONField: React.FC<Props> = (props) => {
validate: memoizedValidate,
})
const handleMount = useCallback(
(editor, monaco) => {
if (!jsonSchema) return
const existingSchemas = monaco.languages.json.jsonDefaults.diagnosticsOptions.schemas || []
const modelUri = monaco.Uri.parse(jsonSchema.uri)
const model = monaco.editor.createModel(JSON.stringify(value, null, 2), 'json', modelUri)
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
enableSchemaRequest: true,
schemas: [...existingSchemas, jsonSchema],
validate: true,
})
editor.setModel(model)
},
[value, jsonSchema],
)
const handleChange = useCallback(
(val) => {
try {
@@ -124,7 +104,6 @@ const JSONField: React.FC<Props> = (props) => {
<CodeEditor
defaultLanguage="json"
onChange={handleChange}
onMount={handleMount}
options={editorOptions}
readOnly={readOnly}
value={stringValue}

View File

@@ -166,8 +166,6 @@ const NumberField: React.FC<Props> = (props) => {
<input
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
max={max}
min={min}
name={path}
onChange={handleChange}
onWheel={(e) => {

View File

@@ -1,5 +1,4 @@
import type { JSONSchema4 } from 'json-schema'
import type { SanitizedConfig } from 'payload/config'
import type { PayloadRequest } from '../../../../../express/types'
import type { RequestContext } from '../../../../../express/types'
@@ -31,14 +30,12 @@ type RichTextAdapterBase<
}) => Promise<void> | null
outputSchema?: ({
collectionIDFieldTypes,
config,
field,
interfaceNameDefinitions,
isRequired,
payload,
}: {
collectionIDFieldTypes: { [key: string]: 'number' | 'string' }
config?: SanitizedConfig
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
/**
* Allows you to define new top-level interfaces that can be re-used in the output schema.

View File

@@ -88,16 +88,14 @@ const TabsField: React.FC<Props> = (props) => {
const tabsPrefKey = `tabs-${indexPath}`
useEffect(() => {
if (preferencesKey) {
const getInitialPref = async () => {
const existingPreferences: DocumentPreferences = await getPreference(preferencesKey)
const initialIndex = path
? existingPreferences?.fields?.[path]?.tabIndex
: existingPreferences?.fields?.[tabsPrefKey]?.tabIndex
setActiveTabIndex(initialIndex || 0)
}
void getInitialPref()
const getInitialPref = async () => {
const existingPreferences: DocumentPreferences = await getPreference(preferencesKey)
const initialIndex = path
? existingPreferences?.fields?.[path]?.tabIndex
: existingPreferences?.fields?.[tabsPrefKey]?.tabIndex
setActiveTabIndex(initialIndex || 0)
}
void getInitialPref()
}, [path, indexPath, getPreference, preferencesKey, tabsPrefKey])
const handleTabChange = useCallback(
@@ -106,30 +104,28 @@ const TabsField: React.FC<Props> = (props) => {
const existingPreferences: DocumentPreferences = await getPreference(preferencesKey)
if (preferencesKey) {
await setPreference(preferencesKey, {
...existingPreferences,
...(path
? {
fields: {
...(existingPreferences?.fields || {}),
[path]: {
...existingPreferences?.fields?.[path],
tabIndex: incomingTabIndex,
},
setPreference(preferencesKey, {
...existingPreferences,
...(path
? {
fields: {
...(existingPreferences?.fields || {}),
[path]: {
...existingPreferences?.fields?.[path],
tabIndex: incomingTabIndex,
},
}
: {
fields: {
...existingPreferences?.fields,
[tabsPrefKey]: {
...existingPreferences?.fields?.[tabsPrefKey],
tabIndex: incomingTabIndex,
},
},
}
: {
fields: {
...existingPreferences?.fields,
[tabsPrefKey]: {
...existingPreferences?.fields?.[tabsPrefKey],
tabIndex: incomingTabIndex,
},
}),
})
}
},
}),
})
},
[preferencesKey, getPreference, setPreference, path, tabsPrefKey],
)

View File

@@ -7,14 +7,14 @@ import CopyToClipboard from '../../../../elements/CopyToClipboard'
import GenerateConfirmation from '../../../../elements/GenerateConfirmation'
import { useFormFields } from '../../../../forms/Form/context'
import Label from '../../../../forms/Label'
import { fieldBaseClass } from '../../../../forms/field-types/shared'
import useField from '../../../../forms/useField'
import { fieldBaseClass } from '../../../../forms/field-types/shared'
const path = 'apiKey'
const baseClass = 'api-key'
const APIKey: React.FC<{ enabled: boolean; readOnly?: boolean }> = ({ enabled, readOnly }) => {
const [initialAPIKey] = useState(uuidv4())
const APIKey: React.FC<{ readOnly?: boolean }> = ({ readOnly }) => {
const [initialAPIKey, setInitialAPIKey] = useState(null)
const [highlightedField, setHighlightedField] = useState(false)
const { t } = useTranslation()
@@ -51,13 +51,14 @@ const APIKey: React.FC<{ enabled: boolean; readOnly?: boolean }> = ({ enabled, r
const { setValue, value } = fieldType
useEffect(() => {
if (!apiKeyValue && enabled) {
setInitialAPIKey(uuidv4())
}, [])
useEffect(() => {
if (!apiKeyValue) {
setValue(initialAPIKey)
}
if (!enabled) {
setValue(null)
}
}, [apiKeyValue, enabled, setValue, initialAPIKey])
}, [apiKeyValue, setValue, initialAPIKey])
useEffect(() => {
if (highlightedField) {
@@ -67,10 +68,6 @@ const APIKey: React.FC<{ enabled: boolean; readOnly?: boolean }> = ({ enabled, r
}
}, [highlightedField])
if (!enabled) {
return null
}
return (
<React.Fragment>
<div className={[fieldBaseClass, 'api-key', 'read-only'].filter(Boolean).join(' ')}>

View File

@@ -145,7 +145,7 @@ const Auth: React.FC<Props> = (props) => {
{useAPIKey && (
<div className={`${baseClass}__api-key`}>
<Checkbox admin={{ readOnly }} label={t('enableAPIKey')} name="enableAPIKey" />
<APIKey enabled={!!enableAPIKey?.value} readOnly={readOnly} />
{enableAPIKey?.value && <APIKey readOnly={readOnly} />}
</div>
)}
{verify && <Checkbox admin={{ readOnly }} label={t('verified')} name="_verified" />}

View File

@@ -26,7 +26,7 @@ export type DefaultEditViewProps = CollectionEditViewProps & {
}
const DefaultEditView: React.FC<DefaultEditViewProps> = (props) => {
const { i18n, t } = useTranslation('general')
const { i18n } = useTranslation('general')
const { refreshCookieAsync, user } = useAuth()
const {
@@ -115,7 +115,7 @@ const DefaultEditView: React.FC<DefaultEditViewProps> = (props) => {
name={`collection-edit--${
typeof collection?.labels?.singular === 'string'
? collection.labels.singular
: t('document')
: 'document'
}`}
type="withoutNav"
/>

View File

@@ -20,6 +20,7 @@ export default [
Field: () => null,
},
},
defaultValue: false,
label: labels['authentication:enableAPIKey'],
},
{
@@ -45,19 +46,16 @@ export default [
hidden: true,
hooks: {
beforeValidate: [
({ data, req, value }) => {
if (data.apiKey === false || data.apiKey === null) {
return null
}
if (data.enableAPIKey === false || data.enableAPIKey === null) {
return null
}
async ({ data, req, value }) => {
if (data.apiKey) {
return crypto
.createHmac('sha1', req.payload.secret)
.update(data.apiKey as string)
.digest('hex')
}
if (data.enableAPIKey === false) {
return null
}
return value
},
],

View File

@@ -8,7 +8,7 @@ import type { PayloadRequest } from '../../express/types'
import type { User } from '../types'
import { buildAfterOperation } from '../../collections/operations/utils'
import { AuthenticationError, LockedAuth, ValidationError } from '../../errors'
import { AuthenticationError, LockedAuth } from '../../errors'
import { afterRead } from '../../fields/hooks/afterRead'
import { commitTransaction } from '../../utilities/commitTransaction'
import getCookieExpiration from '../../utilities/getCookieExpiration'
@@ -86,13 +86,6 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
const { email: unsanitizedEmail, password } = data
if (typeof unsanitizedEmail !== 'string' || unsanitizedEmail.trim() === '') {
throw new ValidationError([{ field: 'email', message: req.i18n.t('validation:required') }])
}
if (typeof password !== 'string' || password.trim() === '') {
throw new ValidationError([{ field: 'password', message: req.i18n.t('validation:required') }])
}
const email = unsanitizedEmail ? unsanitizedEmail.toLowerCase().trim() : null
let user = await payload.db.findOne<any>({

View File

@@ -11,7 +11,6 @@ import registerFirstUserHandler from '../auth/requestHandlers/registerFirstUser'
import resetPassword from '../auth/requestHandlers/resetPassword'
import unlock from '../auth/requestHandlers/unlock'
import verifyEmail from '../auth/requestHandlers/verifyEmail'
import count from './requestHandlers/count'
import create from './requestHandlers/create'
import deleteHandler from './requestHandlers/delete'
import deleteByID from './requestHandlers/deleteByID'
@@ -125,11 +124,6 @@ const buildEndpoints = (collection: SanitizedCollectionConfig): Endpoint[] => {
method: 'post',
path: '/',
},
{
handler: count,
method: 'get',
path: '/count',
},
{
handler: docAccessRequestHandler,
method: 'get',

View File

@@ -169,7 +169,6 @@ const collectionSchema = joi.object().keys({
adminThumbnail: joi.alternatives().try(joi.string(), joi.func()),
crop: joi.bool(),
disableLocalStorage: joi.bool(),
externalFileHeaderFilter: joi.func(),
filesRequiredOnCreate: joi.bool(),
focalPoint: joi.bool(),
formatOptions: joi.object().keys({

View File

@@ -30,7 +30,6 @@ import type { AfterOperationArg, AfterOperationMap } from '../operations/utils'
export type HookOperationType =
| 'autosave'
| 'count'
| 'create'
| 'delete'
| 'forgotPassword'
@@ -342,8 +341,7 @@ export type CollectionAdminOptions = {
useAsTitle?: string
}
/** Manage all aspects of a data collection */
export type CollectionConfig = {
export type BaseCollectionConfig = {
/**
* Access control
*/
@@ -450,6 +448,9 @@ export type CollectionConfig = {
versions?: IncomingCollectionVersions | boolean
}
/** Manage all aspects of a data collection */
export type CollectionConfig = BaseCollectionConfig
export interface SanitizedCollectionConfig
extends Omit<
DeepRequired<CollectionConfig>,
@@ -466,7 +467,6 @@ export type Collection = {
config: SanitizedCollectionConfig
graphQL?: {
JWT: GraphQLObjectType
countType: GraphQLObjectType
mutationInputType: GraphQLNonNull<any>
paginatedType: GraphQLObjectType
type: GraphQLObjectType

View File

@@ -30,10 +30,8 @@ import buildPaginatedListType from '../../graphql/schema/buildPaginatedListType'
import { buildPolicyType } from '../../graphql/schema/buildPoliciesType'
import buildWhereInputType from '../../graphql/schema/buildWhereInputType'
import formatName from '../../graphql/utilities/formatName'
import flattenFields from '../../utilities/flattenTopLevelFields'
import { formatNames, toWords } from '../../utilities/formatLabels'
import { buildVersionCollectionFields } from '../../versions/buildCollectionFields'
import countResolver from './resolvers/count'
import createResolver from './resolvers/create'
import getDeleteResolver from './resolvers/delete'
import { docAccessResolver } from './resolvers/docAccess'
@@ -43,6 +41,7 @@ import findVersionByIDResolver from './resolvers/findVersionByID'
import findVersionsResolver from './resolvers/findVersions'
import restoreVersionResolver from './resolvers/restoreVersion'
import updateResolver from './resolvers/update'
import flattenFields from '../../utilities/flattenTopLevelFields'
function initCollectionsGraphQL(payload: Payload): void {
Object.keys(payload.collections).forEach((slug) => {
@@ -119,9 +118,9 @@ function initCollectionsGraphQL(payload: Payload): void {
if (config.auth && !config.auth.disableLocalStrategy) {
fields.push({
name: 'password',
type: 'text',
label: 'Password',
required: true,
type: 'text',
})
}
@@ -147,7 +146,6 @@ function initCollectionsGraphQL(payload: Payload): void {
}
payload.Query.fields[singularName] = {
type: collection.graphQL.type,
args: {
id: { type: new GraphQLNonNull(idType) },
draft: { type: GraphQLBoolean },
@@ -159,10 +157,10 @@ function initCollectionsGraphQL(payload: Payload): void {
: {}),
},
resolve: findByIDResolver(collection),
type: collection.graphQL.type,
}
payload.Query.fields[pluralName] = {
type: buildPaginatedListType(pluralName, collection.graphQL.type),
args: {
draft: { type: GraphQLBoolean },
where: { type: collection.graphQL.whereInputType },
@@ -177,42 +175,23 @@ function initCollectionsGraphQL(payload: Payload): void {
sort: { type: GraphQLString },
},
resolve: findResolver(collection),
}
payload.Query.fields[`count${pluralName}`] = {
type: new GraphQLObjectType({
name: `count${pluralName}`,
fields: {
totalDocs: { type: GraphQLInt },
},
}),
args: {
draft: { type: GraphQLBoolean },
where: { type: collection.graphQL.whereInputType },
...(payload.config.localization
? {
locale: { type: payload.types.localeInputType },
}
: {}),
},
resolve: countResolver(collection),
type: buildPaginatedListType(pluralName, collection.graphQL.type),
}
payload.Query.fields[`docAccess${singularName}`] = {
type: buildPolicyType({
type: 'collection',
entity: config,
scope: 'docAccess',
typeSuffix: 'DocAccess',
}),
args: {
id: { type: new GraphQLNonNull(idType) },
},
resolve: docAccessResolver(),
type: buildPolicyType({
entity: config,
scope: 'docAccess',
type: 'collection',
typeSuffix: 'DocAccess',
}),
}
payload.Mutation.fields[`create${singularName}`] = {
type: collection.graphQL.type,
args: {
...(createMutationInputType
? { data: { type: collection.graphQL.mutationInputType } }
@@ -225,10 +204,10 @@ function initCollectionsGraphQL(payload: Payload): void {
: {}),
},
resolve: createResolver(collection),
type: collection.graphQL.type,
}
payload.Mutation.fields[`update${singularName}`] = {
type: collection.graphQL.type,
args: {
id: { type: new GraphQLNonNull(idType) },
autosave: { type: GraphQLBoolean },
@@ -243,14 +222,15 @@ function initCollectionsGraphQL(payload: Payload): void {
: {}),
},
resolve: updateResolver(collection),
type: collection.graphQL.type,
}
payload.Mutation.fields[`delete${singularName}`] = {
type: collection.graphQL.type,
args: {
id: { type: new GraphQLNonNull(idType) },
},
resolve: getDeleteResolver(collection),
type: collection.graphQL.type,
}
if (config.versions) {
@@ -263,13 +243,13 @@ function initCollectionsGraphQL(payload: Payload): void {
},
{
name: 'createdAt',
type: 'date',
label: 'Created At',
type: 'date',
},
{
name: 'updatedAt',
type: 'date',
label: 'Updated At',
type: 'date',
},
]
@@ -282,7 +262,6 @@ function initCollectionsGraphQL(payload: Payload): void {
})
payload.Query.fields[`version${formatName(singularName)}`] = {
type: collection.graphQL.versionType,
args: {
id: { type: versionIDType },
...(payload.config.localization
@@ -293,12 +272,9 @@ function initCollectionsGraphQL(payload: Payload): void {
: {}),
},
resolve: findVersionByIDResolver(collection),
type: collection.graphQL.versionType,
}
payload.Query.fields[`versions${pluralName}`] = {
type: buildPaginatedListType(
`versions${formatName(pluralName)}`,
collection.graphQL.versionType,
),
args: {
where: {
type: buildWhereInputType({
@@ -319,13 +295,17 @@ function initCollectionsGraphQL(payload: Payload): void {
sort: { type: GraphQLString },
},
resolve: findVersionsResolver(collection),
type: buildPaginatedListType(
`versions${formatName(pluralName)}`,
collection.graphQL.versionType,
),
}
payload.Mutation.fields[`restoreVersion${formatName(singularName)}`] = {
type: collection.graphQL.type,
args: {
id: { type: versionIDType },
},
resolve: restoreVersionResolver(collection),
type: collection.graphQL.type,
}
}
@@ -335,8 +315,8 @@ function initCollectionsGraphQL(payload: Payload): void {
: [
{
name: 'email',
type: 'email',
required: true,
type: 'email',
},
]
collection.graphQL.JWT = buildObjectType({
@@ -346,8 +326,8 @@ function initCollectionsGraphQL(payload: Payload): void {
...authFields,
{
name: 'collection',
type: 'text',
required: true,
type: 'text',
},
],
parentName: formatName(`${slug}JWT`),
@@ -355,6 +335,7 @@ function initCollectionsGraphQL(payload: Payload): void {
})
payload.Query.fields[`me${singularName}`] = {
resolve: me(collection),
type: new GraphQLObjectType({
name: formatName(`${slug}Me`),
fields: {
@@ -372,15 +353,18 @@ function initCollectionsGraphQL(payload: Payload): void {
},
},
}),
resolve: me(collection),
}
payload.Query.fields[`initialized${singularName}`] = {
type: GraphQLBoolean,
resolve: init(collection.config.slug),
type: GraphQLBoolean,
}
payload.Mutation.fields[`refreshToken${singularName}`] = {
args: {
token: { type: GraphQLString },
},
resolve: refresh(collection),
type: new GraphQLObjectType({
name: formatName(`${slug}Refreshed${singularName}`),
fields: {
@@ -395,29 +379,30 @@ function initCollectionsGraphQL(payload: Payload): void {
},
},
}),
args: {
token: { type: GraphQLString },
},
resolve: refresh(collection),
}
payload.Mutation.fields[`logout${singularName}`] = {
type: GraphQLString,
resolve: logout(collection),
type: GraphQLString,
}
if (!config.auth.disableLocalStrategy) {
if (config.auth.maxLoginAttempts > 0) {
payload.Mutation.fields[`unlock${singularName}`] = {
type: new GraphQLNonNull(GraphQLBoolean),
args: {
email: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: unlock(collection),
type: new GraphQLNonNull(GraphQLBoolean),
}
}
payload.Mutation.fields[`login${singularName}`] = {
args: {
email: { type: GraphQLString },
password: { type: GraphQLString },
},
resolve: login(collection),
type: new GraphQLObjectType({
name: formatName(`${slug}LoginResult`),
fields: {
@@ -432,24 +417,24 @@ function initCollectionsGraphQL(payload: Payload): void {
},
},
}),
args: {
email: { type: GraphQLString },
password: { type: GraphQLString },
},
resolve: login(collection),
}
payload.Mutation.fields[`forgotPassword${singularName}`] = {
type: new GraphQLNonNull(GraphQLBoolean),
args: {
disableEmail: { type: GraphQLBoolean },
email: { type: new GraphQLNonNull(GraphQLString) },
expiration: { type: GraphQLInt },
},
resolve: forgotPassword(collection),
type: new GraphQLNonNull(GraphQLBoolean),
}
payload.Mutation.fields[`resetPassword${singularName}`] = {
args: {
password: { type: GraphQLString },
token: { type: GraphQLString },
},
resolve: resetPassword(collection),
type: new GraphQLObjectType({
name: formatName(`${slug}ResetPassword`),
fields: {
@@ -457,19 +442,14 @@ function initCollectionsGraphQL(payload: Payload): void {
user: { type: collection.graphQL.type },
},
}),
args: {
password: { type: GraphQLString },
token: { type: GraphQLString },
},
resolve: resetPassword(collection),
}
payload.Mutation.fields[`verifyEmail${singularName}`] = {
type: GraphQLBoolean,
args: {
token: { type: GraphQLString },
},
resolve: verifyEmail(collection),
type: GraphQLBoolean,
}
}
}

View File

@@ -1,40 +0,0 @@
import type { PayloadRequest } from '../../../express/types'
import type { Where } from '../../../types'
import type { Collection } from '../../config/types'
import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import count from '../../operations/count'
export type Resolver = (
_: unknown,
args: {
data: Record<string, unknown>
locale?: string
where?: Where
},
context: {
req: PayloadRequest
res: Response
},
) => Promise<{ totalDocs: number }>
export default function findResolver(collection: Collection): Resolver {
return async function resolver(_, args, context) {
let { req } = context
const locale = req.locale
const fallbackLocale = req.fallbackLocale
req = isolateObjectProperty(req, 'locale')
req = isolateObjectProperty(req, 'fallbackLocale')
req.locale = args.locale || locale
req.fallbackLocale = fallbackLocale
const options = {
collection,
req: isolateObjectProperty<PayloadRequest>(req, 'transactionID'),
where: args.where,
}
const results = await count(options)
return results
}
}

View File

@@ -1,113 +0,0 @@
import type { AccessResult } from '../../config/types'
import type { PayloadRequest, Where } from '../../types/index'
import type { Collection, TypeWithID } from '../config/types'
import executeAccess from '../../auth/executeAccess'
import { combineQueries } from '../../database/combineQueries'
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths'
import { commitTransaction } from '../../utilities/commitTransaction'
import { initTransaction } from '../../utilities/initTransaction'
import { killTransaction } from '../../utilities/killTransaction'
import { buildAfterOperation } from './utils'
export type Arguments = {
collection: Collection
disableErrors?: boolean
overrideAccess?: boolean
req?: PayloadRequest
where?: Where
}
async function count<T extends TypeWithID & Record<string, unknown>>(
incomingArgs: Arguments,
): Promise<{ totalDocs: number }> {
let args = incomingArgs
try {
const shouldCommit = await initTransaction(args.req)
// /////////////////////////////////////
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'count',
req: args.req,
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },
disableErrors,
overrideAccess,
req: { payload },
req,
where,
} = args
// /////////////////////////////////////
// Access
// /////////////////////////////////////
let accessResult: AccessResult
if (!overrideAccess) {
accessResult = await executeAccess({ disableErrors, req }, collectionConfig.access.read)
// If errors are disabled, and access returns false, return empty results
if (accessResult === false) {
return {
totalDocs: 0,
}
}
}
let result: { totalDocs: number }
const fullWhere = combineQueries(where, accessResult)
await validateQueryPaths({
collectionConfig,
overrideAccess,
req,
where,
})
result = await payload.db.count({
collection: collectionConfig.slug,
req,
where: fullWhere,
})
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = await buildAfterOperation<T>({
args,
collection: collectionConfig,
operation: 'count',
result,
})
// /////////////////////////////////////
// Return results
// /////////////////////////////////////
if (shouldCommit) await commitTransaction(req)
return result
} catch (error: unknown) {
await killTransaction(args.req)
throw error
}
}
export default count

View File

@@ -1,47 +0,0 @@
import type { GeneratedTypes } from '../../../'
import type { PayloadRequest, RequestContext } from '../../../express/types'
import type { Payload } from '../../../payload'
import type { Document, Where } from '../../../types'
import { APIError } from '../../../errors'
import { createLocalReq } from '../../../utilities/createLocalReq'
import count from '../count'
export type Options<T extends keyof GeneratedTypes['collections']> = {
collection: T
/**
* context, which will then be passed to req.context, which can be read by hooks
*/
context?: RequestContext
disableErrors?: boolean
locale?: string
overrideAccess?: boolean
req?: PayloadRequest
user?: Document
where?: Where
}
export default async function countLocal<T extends keyof GeneratedTypes['collections']>(
payload: Payload,
options: Options<T>,
): Promise<{ totalDocs: number }> {
const { collection: collectionSlug, disableErrors, overrideAccess = true, where } = options
const collection = payload.collections[collectionSlug]
if (!collection) {
throw new APIError(
`The collection with slug ${String(collectionSlug)} can't be found. Find Operation.`,
)
}
const req = createLocalReq(options, payload)
return count<GeneratedTypes['collections'][T]>({
collection,
disableErrors,
overrideAccess,
req,
where,
})
}

View File

@@ -1,5 +1,4 @@
import auth from '../../../auth/operations/local'
import count from './count'
import create from './create'
import deleteLocal from './delete'
import find from './find'
@@ -11,7 +10,6 @@ import update from './update'
export default {
auth,
count,
create,
deleteLocal,
find,

View File

@@ -268,15 +268,14 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
global: null,
operation: 'update',
req,
skipValidation:
Boolean(collectionConfig.versions?.drafts) && data._status !== 'published',
skipValidation: shouldSaveDraft || data._status === 'draft',
})
// /////////////////////////////////////
// Update
// /////////////////////////////////////
if (!shouldSaveDraft || data._status === 'published') {
if (!shouldSaveDraft) {
result = await req.payload.db.updateOne({
id,
collection: collectionConfig.slug,
@@ -298,6 +297,7 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
...result,
createdAt: doc.createdAt,
},
draft: shouldSaveDraft,
payload,
req,
})

View File

@@ -241,7 +241,7 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
global: null,
operation: 'update',
req,
skipValidation: Boolean(collectionConfig.versions?.drafts) && data._status !== 'published',
skipValidation: shouldSaveDraft || data._status === 'draft',
})
// /////////////////////////////////////
@@ -262,7 +262,7 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
// Update
// /////////////////////////////////////
if (!shouldSaveDraft || data._status === 'published') {
if (!shouldSaveDraft) {
result = await req.payload.db.updateOne({
id,
collection: collectionConfig.slug,

View File

@@ -3,7 +3,6 @@ import type login from '../../auth/operations/login'
import type refresh from '../../auth/operations/refresh'
import type { PayloadRequest } from '../../express/types'
import type { AfterOperationHook, SanitizedCollectionConfig, TypeWithID } from '../config/types'
import type countOperation from './count'
import type create from './create'
import type deleteOperation from './delete'
import type deleteByID from './deleteByID'
@@ -13,7 +12,6 @@ import type update from './update'
import type updateByID from './updateByID'
export type AfterOperationMap<T extends TypeWithID> = {
count: typeof countOperation
create: typeof create // todo: pass correct generic
delete: typeof deleteOperation // todo: pass correct generic
deleteByID: typeof deleteByID // todo: pass correct generic
@@ -30,11 +28,6 @@ export type AfterOperationArg<T extends TypeWithID> = {
collection: SanitizedCollectionConfig
req: PayloadRequest
} & (
| {
args: Parameters<AfterOperationMap<T>['count']>[0]
operation: 'count'
result: Awaited<ReturnType<AfterOperationMap<T>['count']>>
}
| {
args: Parameters<AfterOperationMap<T>['create']>[0]
operation: 'create'

View File

@@ -1,26 +0,0 @@
import type { NextFunction, Response } from 'express'
import httpStatus from 'http-status'
import type { PayloadRequest } from '../../express/types'
import type { Where } from '../../types'
import count from '../operations/count'
export default async function countHandler(
req: PayloadRequest,
res: Response,
next: NextFunction,
): Promise<Response<{ totalDocs: number }> | void> {
try {
const result = await count({
collection: req.collection,
req,
where: req.query.where as Where, // This is a little shady
})
return res.status(httpStatus.OK).json(result)
} catch (error) {
return next(error)
}
}

View File

@@ -6,7 +6,6 @@ import type { PayloadRequest } from '../../express/types'
import type { Document } from '../../types'
import { NotFound } from '../../errors'
import { sanitizeCollectionID } from '../../utilities/sanitizeCollectionID'
import deleteByID from '../operations/deleteByID'
export type DeleteResult = {
@@ -19,15 +18,9 @@ export default async function deleteByIDHandler(
res: Response,
next: NextFunction,
): Promise<Response<DeleteResult> | void> {
const id = sanitizeCollectionID({
id: req.params.id,
collectionSlug: req.collection.config.slug,
payload: req.payload,
})
try {
const doc = await deleteByID({
id,
id: req.params.id,
collection: req.collection,
depth: parseInt(String(req.query.depth), 10),
req,

View File

@@ -3,7 +3,6 @@ import type { NextFunction, Response } from 'express'
import type { PayloadRequest } from '../../express/types'
import type { Document } from '../../types'
import { sanitizeCollectionID } from '../../utilities/sanitizeCollectionID'
import findByID from '../operations/findByID'
export type FindByIDResult = {
@@ -16,15 +15,9 @@ export default async function findByIDHandler(
res: Response,
next: NextFunction,
): Promise<Response<FindByIDResult> | void> {
const id = sanitizeCollectionID({
id: req.params.id,
collectionSlug: req.collection.config.slug,
payload: req.payload,
})
try {
const doc = await findByID({
id,
id: req.params.id,
collection: req.collection,
depth: Number(req.query.depth),
draft: req.query.draft === 'true',

View File

@@ -3,7 +3,6 @@ import type { NextFunction, Response } from 'express'
import type { PayloadRequest } from '../../express/types'
import type { Document } from '../../types'
import { sanitizeCollectionID } from '../../utilities/sanitizeCollectionID'
import findVersionByID from '../operations/findVersionByID'
export type FindByIDResult = {
@@ -16,14 +15,8 @@ export default async function findVersionByIDHandler(
res: Response,
next: NextFunction,
): Promise<Response<FindByIDResult> | void> {
const id = sanitizeCollectionID({
id: req.params.id,
collectionSlug: req.collection.config.slug,
payload: req.payload,
})
const options = {
id,
id: req.params.id,
collection: req.collection,
depth: parseInt(String(req.query.depth), 10),
payload: req.payload,

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