Compare commits

...

118 Commits

Author SHA1 Message Date
Elliot DeNolf
4816a1638a chore(release): v3.0.0-beta.18 [skip ci] 2024-04-25 10:31:56 -04:00
Jarrod Flesch
22c53392a3 chore: improves types for payloadRequest (#6012) 2024-04-25 10:23:03 -04:00
Paul
bdaa9e831d chore: add e2e tests for creating first user (#6027) 2024-04-25 10:57:50 -03:00
James Mikrut
036bcd6b8f chore: adds uuid to test (#6030) 2024-04-25 09:51:49 -04:00
Dan Ribbens
4d2bc861cf fix: disable api key beta (#6021) 2024-04-25 09:39:30 -04:00
James Mikrut
98722dc0fd fix(db-postgres): postgres version id bug (#6026) 2024-04-25 09:23:13 -04:00
James Mikrut
629d7c3263 fix(db-postgres): fully functional dbNames (#6023) 2024-04-24 22:42:24 -04:00
James Mikrut
5f7af5317a fix(next): ensures create-first user works (#6020) 2024-04-24 22:23:14 -04:00
James
8bb1b60964 chore: removes unused line 2024-04-24 22:22:40 -04:00
Elliot DeNolf
7ef5493414 ci(scripts): misc improvements 2024-04-24 21:04:12 -04:00
James
a3ac838221 chore: cleanup 2024-04-24 19:52:58 -04:00
James
14400d1cb9 chore: functional create-first-user 2024-04-24 19:48:58 -04:00
James
7d531646fd Merge branch 'fix/create-first-user-pt2' of github.com:payloadcms/payload into fix/create-first-user-pt2 2024-04-24 18:05:02 -04:00
Jacob Fletcher
6f6c1435c7 fix(ui): renders stay logged in modal (#6009) 2024-04-24 16:19:11 -04:00
Elliot DeNolf
332b8b6f34 ci(scripts): true publish with pLimit 2024-04-24 15:26:24 -04:00
Elliot DeNolf
d40a734080 wip: create first user fix 2024-04-24 15:17:13 -04:00
Dan Ribbens
94f1dfef52 fix: bulk publish (#6007) 2024-04-24 15:05:02 -04:00
Elliot DeNolf
0857dbe465 Revert "fix: issues creating the first user (#5986)"
This reverts commit 0ede95f375.
2024-04-24 14:36:08 -04:00
Elliot DeNolf
71f19fba58 chore(release): v3.0.0-beta.15 [skip ci] 2024-04-24 13:41:28 -04:00
Paul
24b18fb0fd feat!: removed getDataAndFile and getLocales from createPayloadRequest in favour of new utilities addDataAndFileToRequest and addLocalesToRequest (#5999) 2024-04-24 13:31:54 -03:00
Elliot DeNolf
5731241a5c fix(db-postgres): postgres uuid (#6003)
Co-authored-by: James <james@trbl.design>
2024-04-24 11:59:39 -04:00
Dan Ribbens
47e70abb4e fix: type collection config missing dbName (#5983) 2024-04-24 11:32:59 -04:00
Paul
0ede95f375 fix: issues creating the first user (#5986)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2024-04-24 11:30:52 -04:00
Jarrod Flesch
b723efdd3b chore: fixing flakey tests (#5984) 2024-04-24 00:44:43 -04:00
Elliot DeNolf
14c513690d ci: lint pr titles (#5988) 2024-04-23 23:40:55 -04:00
Alessio Gravili
88f239e784 feat(richtext-lexical)!: rework population behavior and allow richText adapter field hooks (#5893)
BREAKING:

- Unpopulated lexical relationship, link and upload nodes now save the relationTo document ID under value instead of value.id. This matches the behavior of core relationship fields. This changes the shape of the saved JSON data
- Any custom features which add their own population promises need to be reworked. populationPromises no longer accepts the promises as a return value. Instead, it expects you to mutate the promises array which is passed through, which mimics the way it works in core
2024-04-23 20:43:07 -04:00
Alessio Gravili
a1f6bf8a67 fix(richtext-lexical): Heading feature: enabledHeadingSizes not being applied 2024-04-23 20:37:11 -04:00
Alessio Gravili
912dcd38df fix(richtext-lexical): add missing HorizontalRuleFeature export 2024-04-23 20:25:12 -04:00
Alessio Gravili
da5028cdee feat(richtext-lexical): show loading indicator while block nodes are loading 2024-04-23 20:22:18 -04:00
Elliot DeNolf
899faa62f1 chore: update pnpm-lock 2024-04-23 17:01:57 -04:00
Alessio Gravili
9df6a644c9 chore: update lockfile 2024-04-23 16:37:02 -04:00
Alessio Gravili
1a6d9eaa11 Merge remote-tracking branch 'origin/beta' into fix/lexical-localization 2024-04-23 16:33:54 -04:00
Alessio Gravili
7d447af277 chore: add remaining missing preferences prop to validations 2024-04-23 15:46:01 -04:00
Elliot DeNolf
d8baaab849 chore(release): v3.0.0-beta.14 [skip ci] 2024-04-23 15:29:35 -04:00
Jarrod Flesch
3e1523f007 fix: move graphql-http from devDep to dep in next package (#5982)
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
2024-04-23 15:27:43 -04:00
Alessio Gravili
fa38af025f Merge branch 'beta' into fix/lexical-localization 2024-04-23 15:20:56 -04:00
Elliot DeNolf
6ca9ff847f chore(release): v3.0.0-beta.13 [skip ci] 2024-04-23 15:15:21 -04:00
Alessio Gravili
a8824b2b51 fix: incorrect value for empty preferences passed into buildStateFromSchema 2024-04-23 15:12:55 -04:00
Alessio Gravili
6aa3752b16 feat(richtext-lexical): allow richtext adapters to hook into field hooks 2024-04-23 15:10:35 -04:00
Elliot DeNolf
c483a439bf build: adjust pnpm engines version 2024-04-23 15:01:00 -04:00
Jarrod Flesch
74bdf1c681 chore: reduces graphql dependencies (#5979)
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
2024-04-23 15:00:09 -04:00
James Mikrut
7437d9fe58 Fix/postgres relation names (#5976) 2024-04-23 14:56:43 -04:00
Elliot DeNolf
51f7351962 fix(cpa): install db adapter in package.json (#5921) 2024-04-23 14:03:01 -04:00
Elliot DeNolf
d01fcb921b chore: tsconfig.json back to default 2024-04-23 13:18:45 -04:00
Elliot DeNolf
6179c938bf ci: remove email e2e tests, ethereal calls failing 2024-04-23 13:18:10 -04:00
Elliot DeNolf
dbbcb658a9 fix(deps): proper deps for storage-s3 and storage-vercel-blob (#5975) 2024-04-23 13:17:00 -04:00
James
16f97ad7c3 chore: disables forced pg for tests 2024-04-23 13:13:59 -04:00
James
bc7445ed99 Merge branch 'beta' of github.com:payloadcms/payload into fix/postgres-relation-names 2024-04-23 12:43:32 -04:00
James
e4d024cd0d chore: properly destroys db in postgres 2024-04-23 12:43:25 -04:00
James
1005de8295 fix(db-postgres): shortens relation names 2024-04-23 12:14:01 -04:00
Elliot DeNolf
c79289cedf chore(release): v3.0.0-beta.12 [skip ci] (#5972) 2024-04-23 11:02:08 -04:00
Jarrod Flesch
6a745be036 chore: pass mock req through with validate function to slate richText validation function (#5971) 2024-04-23 10:57:36 -04:00
Elliot DeNolf
cee9cc33ed chore(release): v3.0.0-beta.12 [skip ci] 2024-04-23 10:55:51 -04:00
Elliot DeNolf
9a5e9313cd ci: remove warning for no artifacts found 2024-04-23 10:47:17 -04:00
Elliot DeNolf
5401af5812 chore(storage-*): set disableLocalStorage true for enabled collections (#5970) 2024-04-23 10:46:33 -04:00
Elliot DeNolf
6305a1d1c2 chore: remove NodemailerAdapter type imports 2024-04-23 10:09:35 -04:00
Jarrod Flesch
95b96e3e9e chore: adjust headersWithCors for req without payload (#5963) 2024-04-23 09:50:41 -04:00
Elliot DeNolf
95b3f6d40d chore(scripts): add new packages to getPackageRegistryVersions 2024-04-23 09:10:25 -04:00
Elliot DeNolf
c258a4bef1 chore(scripts): add throttling to release script, optional git commit arg 2024-04-23 09:09:55 -04:00
Elliot DeNolf
647544a0c6 chore: fix build:tests filter [skip ci] 2024-04-23 08:46:15 -04:00
Elliot DeNolf
7e0a2a879c chore: adjust nodemailer type export 2024-04-23 08:39:32 -04:00
Elliot DeNolf
471e1388ae ci: bump pnpm version in gh action, use variable 2024-04-22 22:29:07 -04:00
Elliot DeNolf
1da430b042 ci: bump pnpm version 2024-04-22 22:01:56 -04:00
Elliot DeNolf
56ac06c563 fix: disallow importing from ts extensions 2024-04-22 21:15:13 -04:00
Elliot DeNolf
4dec4bb61c fix: resave media using cloud storage plugin (#5959) 2024-04-22 19:58:57 -04:00
Elliot DeNolf
99a09c49a3 ci: start docker for plugin-cloud-storage e2e 2024-04-22 16:59:57 -04:00
James Mikrut
88fd46bfea fix(db-postgres): row table names were not being built properly (#5960) 2024-04-22 16:55:12 -04:00
Elliot DeNolf
8a6603b3d8 test: add plugin-cloud-storage e2e 2024-04-22 16:43:54 -04:00
PatrikKozak
f6c9f454a5 Merge branch 'beta' of https://github.com/payloadcms/payload into fix/row-table-names 2024-04-22 16:18:24 -04:00
PatrikKozak
d8a5426c37 chore: adds array within row in tabsDoc data 2024-04-22 16:18:14 -04:00
Elliot DeNolf
c9011dcbfd fix(plugin-cloud-storage): resave media 2024-04-22 16:11:19 -04:00
Jarrod Flesch
43089fd13c chore: adds cors headers to routeErrors (#5957) 2024-04-22 15:48:42 -04:00
Elliot DeNolf
bb3bd9c395 chore: adjust email adapter messaging 2024-04-22 15:42:21 -04:00
James
ba423ab424 fix: row table names were not being built properly 2024-04-22 15:10:59 -04:00
Elliot DeNolf
c23984cac3 feat(plugin-cloud-storage): implement storage packages (#5928) 2024-04-22 14:31:20 -04:00
Elliot DeNolf
6685a0fa7e feat!: email adapter (#5901) 2024-04-22 14:26:12 -04:00
Jarrod Flesch
ac4750d016 chore: adds fallbackFileType functionality (#5958) 2024-04-22 14:20:02 -04:00
Elliot DeNolf
951e9fd7f2 test: email e2e updated nodemailer usage 2024-04-22 14:13:38 -04:00
Elliot DeNolf
cbd1554589 chore: adjust email pattern 2024-04-22 13:32:33 -04:00
Jacob Fletcher
80c545933f fix(next): adds CORS headers to API Responses (#5906)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2024-04-22 12:13:06 -04:00
Paul
594f319fc6 chore!: admin now takes a client side custom property and custom is server only (#5926) 2024-04-22 12:22:32 -03:00
Elliot DeNolf
102feb9576 chore: updates telemetry (#5883) 2024-04-22 09:44:34 -04:00
Simon Vreman
68274d2862 fix(plugin-cloud-storage)!: Pass filename to utility function getting file prefix (#5934) 2024-04-21 07:32:11 -04:00
Dan Ribbens
8945b7a4fa fix(db-postgres): nested groups in nested blocks validation (#5941)
Co-authored-by: Ricardo Domingues <rfdomingues98@gmail.com>
2024-04-21 00:38:55 -04:00
Dan Ribbens
d5ef93b2ba fix(db-postgres): v3 #5938 extra version suffix table names (#5940) 2024-04-20 23:23:06 -04:00
Ritsu
cb0f0dba3a chore: removes comment and unused type import (#5935) 2024-04-20 23:06:43 -04:00
Paul
7b263be01b chore: add missing translations (#5929) 2024-04-20 14:57:22 -04:00
Dan Ribbens
56df60f520 chore: fixes e2e test running on windows (#5927) 2024-04-20 14:54:18 -04:00
Ritsu
d5cbbc472d feat: add count operation to collections (#5930) 2024-04-20 14:45:44 -04:00
Dan Ribbens
d987e5628a feat(live-preview-vue): new live-preview-vue package (#5933)
Co-authored-by: Christian Gil <mrcgam.christian@gmail.com>
2024-04-20 07:52:00 -04:00
Dan Ribbens
1383191f15 fix: v3 update many with drafts (#5900)
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
2024-04-19 16:32:59 -04:00
Ritsu
27297284cf fix: Passes correct path to import modules on Windows started with file:// (#5919) 2024-04-19 16:28:41 -04:00
Kendell Joseph
3af3a91c87 feat: json field schemas (#5898) 2024-04-19 13:35:59 -04:00
Paul
23c5b71f95 chore(payload,ui)!:update custom config to separate client and server bundles (#5914) 2024-04-19 11:52:55 -03:00
Dan Ribbens
2ee6a8ec3a fix(db-mongodb): ignore end session errors (#5905) 2024-04-19 09:19:55 -04:00
Elliot DeNolf
10819b8693 chore: proper SendMailOptions export 2024-04-18 15:43:10 -04:00
Elliot DeNolf
83c617b452 test: clean up email-nodemailer config 2024-04-18 14:17:36 -04:00
Elliot DeNolf
4acb133655 chore: export SendMailOptions 2024-04-18 14:15:55 -04:00
Elliot DeNolf
6e4135e790 test: add nodemailer adapter to email test config 2024-04-18 13:36:48 -04:00
Elliot DeNolf
f0198b62f3 feat: implement stdout email adapter, use if no adapter configured 2024-04-18 11:59:03 -04:00
Jessica Chowdhury
3ff8063ab8 chore:(i18n): adds translation for document/s key (#5890) 2024-04-18 10:18:06 +01:00
Elliot DeNolf
8d52f1b279 chore: add payload to dev deps 2024-04-18 02:27:43 -04:00
Elliot DeNolf
24072d222c chore: clean up types, remove logMockEmailCredentials 2024-04-18 02:07:54 -04:00
Elliot DeNolf
55c59e71da chore: remove nodemailer from payload completely 2024-04-18 01:44:35 -04:00
Elliot DeNolf
62233788e0 feat(plugin-cloud): use nodemailer adapter 2024-04-18 01:44:20 -04:00
Elliot DeNolf
b297c5499d chore(email): strict true 2024-04-18 00:02:05 -04:00
Elliot DeNolf
fb7925f272 feat: create email-nodemailer package 2024-04-17 21:58:24 -04:00
Patrik
221e873862 chore(translations): adds localsNotSaved_one & localsNotSaved_other translations (#5903) 2024-04-17 16:34:10 -04:00
Patrik
e7143e02e2 fix: adds type error validations for email and password in login operation (#5899) 2024-04-17 16:33:19 -04:00
Elliot DeNolf
a1d68bd951 feat: abstract nodemailer into email adapter interface 2024-04-17 16:10:51 -04:00
Jarrod Flesch
93ee452a2d fix(next): do not require handlers, attempt to read filesystem or throw (#5896) 2024-04-17 15:12:57 -04:00
Jarrod Flesch
1abaa5fc17 chore(next): bump next@^14.3.0-canary.7 (#5894) 2024-04-17 13:07:40 -04:00
Alessio Gravili
999059bc61 fix(richtext-lexical): properly validate block node nested fields, fixes one failing e2e test suite we previously skipped 2024-04-17 11:47:24 -04:00
Alessio Gravili
39ba39c237 feat(richtext-lexical)!: rework how population works and saves data, improve node typing 2024-04-17 11:46:47 -04:00
Jarrod Flesch
009e6c2066 chore(test): fix flakey relationship tests (#5892) 2024-04-17 11:44:07 -04:00
Ritsu
8bf03ae706 fix(next): pass a corrent content-type header in getFile route (#5799)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2024-04-17 11:40:02 -04:00
Alessio Gravili
58ea94f6ac feat: pass through doc preferences to field validate function and improve types 2024-04-17 09:27:04 -04:00
Alessio Gravili
a2afc38894 fix(richtext-lexical): do not allow empty url field in link drawer 2024-04-16 11:03:12 -04:00
622 changed files with 9210 additions and 2676 deletions

View File

@@ -12,6 +12,7 @@ concurrency:
env:
NODE_VERSION: 18.20.2
PNPM_VERSION: 8.15.7
jobs:
changes:
@@ -68,7 +69,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Get pnpm store directory
@@ -113,7 +114,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Restore build
@@ -137,9 +138,9 @@ jobs:
database:
- mongodb
- postgres
# - postgres-custom-schema
# - postgres-uuid
# - supabase
- postgres-custom-schema
- postgres-uuid
- supabase
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
@@ -162,7 +163,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Restore build
@@ -236,7 +237,6 @@ jobs:
- access-control
- admin
- auth
- email
- field-error-states
- fields-relationship
- fields
@@ -246,6 +246,7 @@ jobs:
- fields__collections__Lexical
- live-preview
- localization
- plugin-cloud-storage
- plugin-form-builder
- plugin-nested-docs
- plugin-seo
@@ -265,7 +266,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Restore build
@@ -275,6 +276,10 @@ jobs:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Start LocalStack
run: pnpm docker:start
if: ${{ matrix.suite == 'plugin-cloud-storage' }}
- name: Install Playwright
run: pnpm exec playwright install --with-deps
@@ -286,6 +291,7 @@ jobs:
with:
name: test-results-${{ matrix.suite }}
path: test/test-results/
if-no-files-found: ignore
retention-days: 1
tests-type-generation:
@@ -306,7 +312,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Restore build

97
.github/workflows/pr-title.yml vendored Normal file
View File

@@ -0,0 +1,97 @@
name: pr-title
on:
pull_request:
types:
- opened
- edited
- synchronize
permissions:
pull-requests: write
jobs:
main:
name: lint-pr-title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
types: |
build
chore
ci
docs
feat
fix
perf
refactor
revert
style
test
types
scopes: |
cpa
db-\*
db-mongodb
db-postgres
email-nodemailer
eslint
graphql
live-preview
live-preview-react
next
payload
plugin-cloud
plugin-cloud-storage
plugin-form-builder
plugin-nested-docs
plugin-redirects
plugin-search
plugin-sentry
plugin-seo
plugin-stripe
richtext-\*
richtext-lexical
richtext-slate
storage-\*
storage-azure
storage-gcs
storage-vercel-blob
storage-s3
translations
ui
templates
examples
# Disallow uppercase letters at the beginning of the subject
subjectPattern: ^(?![A-Z]).+$
- uses: marocchino/sticky-pull-request-comment@v2
# When the previous steps fails, the workflow would stop. By adding this
# condition you can continue the execution with the populated error message.
if: always() && (steps.lint_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
message: |
Pull Request titles must follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and have valid scopes.
${{ steps.lint_pr_title.outputs.error_message }}
```
feat(ui): add Button component
^ ^ ^
| | |__ Subject
| |_______ Scope
|____________ Type
```
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-title-lint-error
delete: true

View File

@@ -1,9 +1,10 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { REST_DELETE, REST_GET, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
export const GET = REST_GET(config)
export const POST = REST_POST(config)
export const DELETE = REST_DELETE(config)
export const PATCH = REST_PATCH(config)
export const OPTIONS = REST_OPTIONS(config)

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, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
keywords: json, jsonSchema, schema, validation, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
---
<Banner>
@@ -30,6 +30,7 @@ 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) |
@@ -52,7 +53,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'
@@ -68,3 +69,67 @@ 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

@@ -42,11 +42,12 @@ export const PublicUser: CollectionConfig = {
**Payload will automatically open up the following queries:**
| Query Name | Operation |
| ------------------ | ------------------- |
| **`PublicUser`** | `findByID` |
| **`PublicUsers`** | `find` |
| **`mePublicUser`** | `me` auth operation |
| Query Name | Operation |
| ------------------ | ------------------- |
| **`PublicUser`** | `findByID` |
| **`PublicUsers`** | `find` |
| **`countPublicUsers`** | `count` |
| **`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 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.
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.
By default, all hooks accept the following args:
@@ -36,6 +36,10 @@ And return the following values:
For example, `data?.relatedPosts?.[0]?.title`.
</Banner>
<Banner type="info">
It 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.
@@ -71,11 +75,40 @@ export const PageClient: React.FC<{
}
```
<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>
### 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>
```
## Building your own hook

View File

@@ -164,6 +164,22 @@ 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,6 +90,19 @@ 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

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.11",
"version": "3.0.0-beta.18",
"private": true,
"type": "module",
"workspaces:": [
@@ -18,6 +18,7 @@
"build:create-payload-app": "turbo build --filter create-payload-app",
"build:db-mongodb": "turbo build --filter db-mongodb",
"build:db-postgres": "turbo build --filter db-postgres",
"build:email-nodemailer": "turbo build --filter email-nodemailer",
"build:eslint-config-payload": "turbo build --filter eslint-config-payload",
"build:graphql": "turbo build --filter graphql",
"build:live-preview": "turbo build --filter live-preview",
@@ -35,7 +36,7 @@
"build:plugin-stripe": "turbo build --filter plugin-stripe",
"build:richtext-lexical": "turbo build --filter richtext-lexical",
"build:richtext-slate": "turbo build --filter richtext-slate",
"build:tests": "pnpm --filter test run typecheck",
"build:tests": "pnpm --filter payload-test-suite run typecheck",
"build:translations": "turbo build --filter translations",
"build:ui": "turbo build --filter ui",
"clean": "turbo clean",
@@ -127,7 +128,7 @@
"lint-staged": "^14.0.1",
"minimist": "1.2.8",
"mongodb-memory-server": "^9.0",
"next": "^14.2.0-canary.23",
"next": "^14.3.0-canary.7",
"node-mocks-http": "^1.14.1",
"nodemon": "3.0.3",
"open": "^10.1.0",
@@ -164,7 +165,7 @@
},
"engines": {
"node": ">=18.20.2",
"pnpm": ">=8"
"pnpm": "^8.15.7"
},
"lint-staged": {
"*.{md,mdx,yml,json}": "prettier --write",
@@ -175,6 +176,7 @@
},
"dependencies": {
"@sentry/react": "^7.77.0",
"ajv": "^8.12.0",
"passport-strategy": "1.0.0"
},
"pnpm": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.11",
"version": "3.0.0-beta.18",
"license": "MIT",
"type": "module",
"homepage": "https://payloadcms.com",

View File

@@ -1,5 +1,9 @@
import fse from 'fs-extra'
import globby from 'globby'
import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
import type { DbDetails } from '../types.js'
@@ -15,6 +19,34 @@ export async function configurePayloadConfig(args: {
return
}
// Update package.json
const packageJsonPath =
'projectDir' in args.projectDirOrConfigPath &&
path.resolve(args.projectDirOrConfigPath.projectDir, 'package.json')
if (packageJsonPath && fse.existsSync(packageJsonPath)) {
try {
const packageObj = await fse.readJson(packageJsonPath)
const dbPackage = dbReplacements[args.dbDetails.type]
// Delete all other db adapters
Object.values(dbReplacements).forEach((p) => {
if (p.packageName !== dbPackage.packageName) {
delete packageObj.dependencies[p.packageName]
}
})
// Set version of db adapter to match payload version
packageObj.dependencies[dbPackage.packageName] = packageObj.dependencies['payload']
await fse.writeJson(packageJsonPath, packageObj, { spaces: 2 })
} catch (err: unknown) {
warning(`Unable to configure Payload in package.json`)
warning(err instanceof Error ? err.message : '')
}
}
try {
let payloadConfigPath: string | undefined
if (!('payloadConfigPath' in args.projectDirOrConfigPath)) {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.0.0-beta.11",
"version": "3.0.0-beta.18",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": {
"type": "git",

View File

@@ -0,0 +1,49 @@
import type { QueryOptions } from 'mongoose'
import type { Count } from 'payload/database'
import type { PayloadRequestWithData } from 'payload/types'
import { flattenWhereToOperators } from 'payload/database'
import type { MongooseAdapter } from './index.js'
import { withSession } from './withSession.js'
export const count: Count = async function count(
this: MongooseAdapter,
{ collection, locale, req = {} as PayloadRequestWithData, 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

@@ -1,5 +1,5 @@
import type { Create } from 'payload/database'
import type { Document, PayloadRequest } from 'payload/types'
import type { Document, PayloadRequestWithData } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -8,7 +8,7 @@ import { withSession } from './withSession.js'
export const create: Create = async function create(
this: MongooseAdapter,
{ collection, data, req = {} as PayloadRequest },
{ collection, data, req = {} as PayloadRequestWithData },
) {
const Model = this.collections[collection]
const options = withSession(this, req.transactionID)

View File

@@ -1,5 +1,5 @@
import type { CreateGlobal } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -8,7 +8,7 @@ import { withSession } from './withSession.js'
export const createGlobal: CreateGlobal = async function createGlobal(
this: MongooseAdapter,
{ slug, data, req = {} as PayloadRequest },
{ slug, data, req = {} as PayloadRequestWithData },
) {
const Model = this.globals
const global = {

View File

@@ -1,5 +1,5 @@
import type { CreateGlobalVersion } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import type { Document } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -8,7 +8,15 @@ import { withSession } from './withSession.js'
export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
this: MongooseAdapter,
{ autosave, createdAt, globalSlug, parent, req = {} as PayloadRequest, updatedAt, versionData },
{
autosave,
createdAt,
globalSlug,
parent,
req = {} as PayloadRequestWithData,
updatedAt,
versionData,
},
) {
const VersionModel = this.versions[globalSlug]
const options = withSession(this, req.transactionID)

View File

@@ -1,5 +1,5 @@
import type { CreateVersion } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import type { Document } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -13,7 +13,7 @@ export const createVersion: CreateVersion = async function createVersion(
collectionSlug,
createdAt,
parent,
req = {} as PayloadRequest,
req = {} as PayloadRequestWithData,
updatedAt,
versionData,
},

View File

@@ -1,5 +1,5 @@
import type { DeleteMany } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -7,7 +7,7 @@ import { withSession } from './withSession.js'
export const deleteMany: DeleteMany = async function deleteMany(
this: MongooseAdapter,
{ collection, req = {} as PayloadRequest, where },
{ collection, req = {} as PayloadRequestWithData, where },
) {
const Model = this.collections[collection]
const options = {

View File

@@ -1,5 +1,5 @@
import type { DeleteOne } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import type { Document } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -9,7 +9,7 @@ import { withSession } from './withSession.js'
export const deleteOne: DeleteOne = async function deleteOne(
this: MongooseAdapter,
{ collection, req = {} as PayloadRequest, where },
{ collection, req = {} as PayloadRequestWithData, where },
) {
const Model = this.collections[collection]
const options = withSession(this, req.transactionID)

View File

@@ -1,5 +1,5 @@
import type { DeleteVersions } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -7,7 +7,7 @@ import { withSession } from './withSession.js'
export const deleteVersions: DeleteVersions = async function deleteVersions(
this: MongooseAdapter,
{ collection, locale, req = {} as PayloadRequest, where },
{ collection, locale, req = {} as PayloadRequestWithData, where },
) {
const VersionsModel = this.versions[collection]
const options = {

View File

@@ -1,6 +1,6 @@
import type { PaginateOptions } from 'mongoose'
import type { Find } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import { flattenWhereToOperators } from 'payload/database'
@@ -12,7 +12,16 @@ import { withSession } from './withSession.js'
export const find: Find = async function find(
this: MongooseAdapter,
{ collection, limit, locale, page, pagination, req = {} as PayloadRequest, sort: sortArg, where },
{
collection,
limit,
locale,
page,
pagination,
req = {} as PayloadRequestWithData,
sort: sortArg,
where,
},
) {
const Model = this.collections[collection]
const collectionConfig = this.payload.collections[collection].config

View File

@@ -1,5 +1,5 @@
import type { FindGlobal } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import { combineQueries } from 'payload/database'
@@ -10,7 +10,7 @@ import { withSession } from './withSession.js'
export const findGlobal: FindGlobal = async function findGlobal(
this: MongooseAdapter,
{ slug, locale, req = {} as PayloadRequest, where },
{ slug, locale, req = {} as PayloadRequestWithData, where },
) {
const Model = this.globals
const options = {

View File

@@ -1,6 +1,6 @@
import type { PaginateOptions } from 'mongoose'
import type { FindGlobalVersions } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import { flattenWhereToOperators } from 'payload/database'
import { buildVersionGlobalFields } from 'payload/versions'
@@ -19,7 +19,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
locale,
page,
pagination,
req = {} as PayloadRequest,
req = {} as PayloadRequestWithData,
skip,
sort: sortArg,
where,

View File

@@ -1,6 +1,6 @@
import type { MongooseQueryOptions } from 'mongoose'
import type { FindOne } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import type { Document } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -10,7 +10,7 @@ import { withSession } from './withSession.js'
export const findOne: FindOne = async function findOne(
this: MongooseAdapter,
{ collection, locale, req = {} as PayloadRequest, where },
{ collection, locale, req = {} as PayloadRequestWithData, where },
) {
const Model = this.collections[collection]
const options: MongooseQueryOptions = {

View File

@@ -1,6 +1,6 @@
import type { PaginateOptions } from 'mongoose'
import type { FindVersions } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import { flattenWhereToOperators } from 'payload/database'
@@ -18,7 +18,7 @@ export const findVersions: FindVersions = async function findVersions(
locale,
page,
pagination,
req = {} as PayloadRequest,
req = {} as PayloadRequestWithData,
skip,
sort: sortArg,
where,

View File

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

View File

@@ -1,4 +1,4 @@
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import {
commitTransaction,
@@ -50,7 +50,7 @@ export async function migrateFresh(
msg: `Found ${migrationFiles.length} migration files.`,
})
const req = { payload } as PayloadRequest
const req = { payload } as PayloadRequestWithData
// Run all migrate up
for (const migration of migrationFiles) {

View File

@@ -1,6 +1,6 @@
import type { PaginateOptions } from 'mongoose'
import type { QueryDrafts } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import { combineQueries, flattenWhereToOperators } from 'payload/database'
@@ -12,7 +12,16 @@ import { withSession } from './withSession.js'
export const queryDrafts: QueryDrafts = async function queryDrafts(
this: MongooseAdapter,
{ collection, limit, locale, page, pagination, req = {} as PayloadRequest, sort: sortArg, where },
{
collection,
limit,
locale,
page,
pagination,
req = {} as PayloadRequestWithData,
sort: sortArg,
where,
},
) {
const VersionModel = this.versions[collection]
const collectionConfig = this.payload.collections[collection].config

View File

@@ -6,6 +6,10 @@ export const commitTransaction: CommitTransaction = async function commitTransac
}
await this.sessions[id].commitTransaction()
await this.sessions[id].endSession()
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
}
delete this.sessions[id]
}

View File

@@ -1,5 +1,5 @@
import type { UpdateGlobal } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -8,7 +8,7 @@ import { withSession } from './withSession.js'
export const updateGlobal: UpdateGlobal = async function updateGlobal(
this: MongooseAdapter,
{ slug, data, req = {} as PayloadRequest },
{ slug, data, req = {} as PayloadRequestWithData },
) {
const Model = this.globals
const options = {

View File

@@ -1,5 +1,5 @@
import type { UpdateGlobalVersionArgs } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import type { PayloadRequestWithData, TypeWithID } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -11,7 +11,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
id,
global,
locale,
req = {} as PayloadRequest,
req = {} as PayloadRequestWithData,
versionData,
where,
}: UpdateGlobalVersionArgs<T>,

View File

@@ -1,5 +1,5 @@
import type { UpdateOne } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -9,7 +9,7 @@ import { withSession } from './withSession.js'
export const updateOne: UpdateOne = async function updateOne(
this: MongooseAdapter,
{ id, collection, data, locale, req = {} as PayloadRequest, where: whereArg },
{ id, collection, data, locale, req = {} as PayloadRequestWithData, where: whereArg },
) {
const where = id ? { id: { equals: id } } : whereArg
const Model = this.collections[collection]

View File

@@ -1,5 +1,5 @@
import type { UpdateVersion } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import type { MongooseAdapter } from './index.js'
@@ -7,7 +7,7 @@ import { withSession } from './withSession.js'
export const updateVersion: UpdateVersion = async function updateVersion(
this: MongooseAdapter,
{ id, collection, locale, req = {} as PayloadRequest, versionData, where },
{ id, collection, locale, req = {} as PayloadRequestWithData, versionData, where },
) {
const VersionModel = this.versions[collection]
const whereToUse = where || { id: { equals: id } }

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-beta.11",
"version": "3.0.0-beta.18",
"description": "The officially supported Postgres database adapter for Payload",
"repository": {
"type": "git",

View File

@@ -0,0 +1,62 @@
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.js'
import type { PostgresAdapter } from './types.js'
import { chainMethods } from './find/chainMethods.js'
import buildQuery from './queries/buildQuery.js'
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

@@ -1,8 +1,9 @@
import type { Create } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export const create: Create = async function create(
@@ -12,6 +13,8 @@ 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,
@@ -19,10 +22,7 @@ export const create: Create = async function create(
fields: collection.fields,
operation: 'create',
req,
tableName: getTableName({
adapter: this,
config: collection,
}),
tableName,
})
return result

View File

@@ -1,18 +1,21 @@
import type { CreateGlobalArgs } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import type { PayloadRequestWithData, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function createGlobal<T extends TypeWithID>(
this: PostgresAdapter,
{ slug, data, req = {} as PayloadRequest }: CreateGlobalArgs,
{ slug, data, req = {} as PayloadRequestWithData }: CreateGlobalArgs,
): 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 result = await upsertRow<T>({
adapter: this,
data,
@@ -20,10 +23,7 @@ export async function createGlobal<T extends TypeWithID>(
fields: globalConfig.fields,
operation: 'create',
req,
tableName: getTableName({
adapter: this,
config: globalConfig,
}),
tableName,
})
return result

View File

@@ -1,26 +1,28 @@
import type { TypeWithVersion } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import type { PayloadRequestWithData, 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.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function createGlobalVersion<T extends TypeWithID>(
this: PostgresAdapter,
{ autosave, globalSlug, req = {} as PayloadRequest, versionData }: CreateGlobalVersionArgs,
{
autosave,
globalSlug,
req = {} as PayloadRequestWithData,
versionData,
}: CreateGlobalVersionArgs,
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const global = this.payload.globals.config.find(({ slug }) => slug === globalSlug)
const tableName = getTableName({
adapter: this,
config: global,
versions: true,
})
const tableName = this.tableNameMap.get(`_${toSnakeCase(global.slug)}${this.versionsSuffix}`)
const result = await upsertRow<TypeWithVersion<T>>({
adapter: this,

View File

@@ -1,12 +1,12 @@
import type { CreateVersionArgs, TypeWithVersion } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import type { PayloadRequestWithData, 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.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function createVersion<T extends TypeWithID>(
@@ -15,17 +15,18 @@ export async function createVersion<T extends TypeWithID>(
autosave,
collectionSlug,
parent,
req = {} as PayloadRequest,
req = {} as PayloadRequestWithData,
versionData,
}: CreateVersionArgs<T>,
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = getTableName({
adapter: this,
config: collection,
versions: true,
})
const defaultTableName = toSnakeCase(collection.slug)
const tableName = this.tableNameMap.get(`_${defaultTableName}${this.versionsSuffix}`)
const version = { ...versionData }
if (version.id) delete version.id
const result = await upsertRow<TypeWithVersion<T>>({
adapter: this,
@@ -33,7 +34,7 @@ export async function createVersion<T extends TypeWithID>(
autosave,
latest: true,
parent,
version: versionData,
version,
},
db,
fields: buildVersionCollectionFields(collection),
@@ -43,15 +44,9 @@ export async function createVersion<T extends TypeWithID>(
})
const table = this.tables[tableName]
const relationshipsTable =
this.tables[
getTableName({
adapter: this,
config: collection,
relationships: true,
versions: true,
})
]
this.tables[`_${defaultTableName}${this.versionsSuffix}${this.relationshipsSuffix}`]
if (collection.versions.drafts) {
await db.execute(sql`

View File

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

View File

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

View File

@@ -1,26 +1,25 @@
import type { DeleteVersions } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import type { PayloadRequestWithData, 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.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const deleteVersions: DeleteVersions = async function deleteVersion(
this: PostgresAdapter,
{ collection, locale, req = {} as PayloadRequest, where: where },
{ collection, locale, req = {} as PayloadRequestWithData, where: where },
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const tableName = this.tableNameMap.get(
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
)
const fields = buildVersionCollectionFields(collectionConfig)
const { docs } = await findMany({

View File

@@ -2,13 +2,12 @@ import type { Destroy } from 'payload/database'
import type { PostgresAdapter } from './types.js'
import { pushDevSchema } from './utilities/pushDevSchema.js'
// eslint-disable-next-line @typescript-eslint/require-await
export const destroy: Destroy = async function destroy(this: PostgresAdapter) {
if (process.env.NODE_ENV !== 'production') {
await pushDevSchema(this)
} else {
// TODO: this hangs test suite for some reason
// await this.pool.end()
}
this.enums = {}
this.schema = {}
this.tables = {}
this.relations = {}
this.fieldConstraints = {}
this.drizzle = undefined
}

View File

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

View File

@@ -1,5 +1,5 @@
import type { FindArgs } from 'payload/database'
import type { Field, PayloadRequest, TypeWithID } from 'payload/types'
import type { Field, PayloadRequestWithData, TypeWithID } from 'payload/types'
import { inArray, sql } from 'drizzle-orm'
@@ -25,7 +25,7 @@ export const findMany = async function find({
locale,
page = 1,
pagination,
req = {} as PayloadRequest,
req = {} as PayloadRequestWithData,
skip,
sort,
tableName,

View File

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

View File

@@ -1,19 +1,18 @@
import type { FindGlobal } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
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 = getTableName({
adapter: this,
config: globalConfig,
})
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))
const {
docs: [doc],

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
import type { FindVersions } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import type { PayloadRequestWithData, SanitizedCollectionConfig } from 'payload/types'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const findVersions: FindVersions = async function findVersions(
this: PostgresAdapter,
@@ -16,7 +16,7 @@ export const findVersions: FindVersions = async function findVersions(
locale,
page,
pagination,
req = {} as PayloadRequest,
req = {} as PayloadRequestWithData,
skip,
sort: sortArg,
where,
@@ -25,11 +25,10 @@ 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 = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const tableName = this.tableNameMap.get(
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
)
const fields = buildVersionCollectionFields(collectionConfig)
return findMany({

View File

@@ -8,6 +8,7 @@ import { createDatabaseAdapter } from 'payload/database'
import type { Args, PostgresAdapter } from './types.js'
import { connect } from './connect.js'
import { count } from './count.js'
import { create } from './create.js'
import { createGlobal } from './createGlobal.js'
import { createGlobalVersion } from './createGlobalVersion.js'
@@ -44,16 +45,13 @@ export { sql } from 'drizzle-orm'
export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter> {
const postgresIDType = args.idType || 'serial'
const payloadIDType = postgresIDType ? 'number' : 'text'
const payloadIDType = postgresIDType === 'serial' ? 'number' : 'text'
function adapter({ payload }: { payload: Payload }) {
const migrationDir = findMigrationDir(args.migrationDir)
return createDatabaseAdapter<PostgresAdapter>({
name: 'postgres',
// Postgres-specific
blockTableNames: {},
drizzle: undefined,
enums: {},
fieldConstraints: {},
@@ -69,6 +67,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
schema: {},
schemaName: args.schemaName,
sessions: {},
tableNameMap: new Map<string, string>(),
tables: {},
versionsSuffix: args.versionsSuffix || '_v',
@@ -76,6 +75,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
beginTransaction,
commitTransaction,
connect,
count,
create,
createGlobal,
createGlobalVersion,

View File

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

View File

@@ -1,7 +1,7 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import type { Payload } from 'payload'
import type { Migration } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import { createRequire } from 'module'
import {
@@ -88,7 +88,7 @@ async function runMigrationFile(payload: Payload, migration: Migration, batch: n
const { generateDrizzleJson } = require('drizzle-kit/payload')
const start = Date.now()
const req = { payload } as PayloadRequest
const req = { payload } as PayloadRequestWithData
payload.logger.info({ msg: `Migrating: ${migration.name}` })

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import {
commitTransaction,
@@ -40,7 +40,7 @@ export async function migrateDown(this: PostgresAdapter): Promise<void> {
}
const start = Date.now()
const req = { payload } as PayloadRequest
const req = { payload } as PayloadRequestWithData
try {
payload.logger.info({ msg: `Migrating down: ${migrationFile.name}` })

View File

@@ -1,4 +1,4 @@
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import { sql } from 'drizzle-orm'
import {
@@ -56,7 +56,7 @@ export async function migrateFresh(
msg: `Found ${migrationFiles.length} migration files.`,
})
const req = { payload } as PayloadRequest
const req = { payload } as PayloadRequestWithData
// Run all migrate up
for (const migration of migrationFiles) {
payload.logger.info({ msg: `Migrating: ${migration.name}` })

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import {
commitTransaction,
@@ -34,7 +34,7 @@ export async function migrateRefresh(this: PostgresAdapter) {
msg: `Rolling back batch ${latestBatch} consisting of ${existingMigrations.length} migration(s).`,
})
const req = { payload } as PayloadRequest
const req = { payload } as PayloadRequestWithData
// Reverse order of migrations to rollback
existingMigrations.reverse()

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import {
commitTransaction,
@@ -27,7 +27,7 @@ export async function migrateReset(this: PostgresAdapter): Promise<void> {
return
}
const req = { payload } as PayloadRequest
const req = { payload } as PayloadRequestWithData
// Rollback all migrations in order
for (const migration of existingMigrations) {

View File

@@ -14,8 +14,6 @@ import { v4 as uuid } from 'uuid'
import type { GenericColumn, GenericTable, PostgresAdapter } from '../types.js'
import type { BuildQueryJoinAliases, BuildQueryJoins } from './buildQuery.js'
import { getTableName } from '../schema/getTableName.js'
type Constraint = {
columnName: string
table: GenericTable | PgTableWithColumns<any>
@@ -185,13 +183,7 @@ export const getTableColumnFromPath = ({
case 'group': {
if (locale && field.localized && adapter.payload.config.localization) {
newTableName = getTableName({
adapter,
config: field,
locales: true,
parentTableName: tableName,
prefix: `${tableName}_`,
})
newTableName = `${tableName}${adapter.localesSuffix}`
joins[tableName] = eq(
adapter.tables[tableName].id,
@@ -227,12 +219,9 @@ export const getTableColumnFromPath = ({
case 'select': {
if (field.hasMany) {
newTableName = getTableName({
adapter,
config: field,
parentTableName: `${tableName}_${tableNameSuffix}`,
prefix: `${tableName}_${tableNameSuffix}`,
})
const newTableName = adapter.tableNameMap.get(
`${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`,
)
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
@@ -305,12 +294,10 @@ export const getTableColumnFromPath = ({
}
case 'array': {
newTableName = getTableName({
adapter,
config: field,
parentTableName: `${tableName}_${tableNameSuffix}`,
prefix: `${tableName}_${tableNameSuffix}`,
})
newTableName = adapter.tableNameMap.get(
`${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`,
)
constraintPath = `${constraintPath}${field.name}.%.`
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
@@ -357,12 +344,11 @@ export const getTableColumnFromPath = ({
const blockTypes = Array.isArray(value) ? value : [value]
blockTypes.forEach((blockType) => {
const block = field.blocks.find((block) => block.slug === blockType)
newTableName = getTableName({
adapter,
config: block,
parentTableName: tableName,
prefix: `${tableName}_blocks_`,
})
newTableName = adapter.tableNameMap.get(
`${tableName}_blocks_${toSnakeCase(block.slug)}`,
)
joins[newTableName] = eq(
adapter.tables[tableName].id,
adapter.tables[newTableName]._parentID,
@@ -382,13 +368,9 @@ export const getTableColumnFromPath = ({
}
const hasBlockField = field.blocks.some((block) => {
newTableName = getTableName({
adapter,
config: block,
parentTableName: tableName,
prefix: `${tableName}_blocks_`,
})
newTableName = adapter.tableNameMap.get(`${tableName}_blocks_${toSnakeCase(block.slug)}`)
constraintPath = `${constraintPath}${field.name}.%.`
let result
const blockConstraints = []
const blockSelectFields = {}
@@ -495,10 +477,9 @@ export const getTableColumnFromPath = ({
if (typeof field.relationTo === 'string') {
const relationshipConfig = adapter.payload.collections[field.relationTo].config
newTableName = getTableName({
adapter,
config: relationshipConfig,
})
newTableName = adapter.tableNameMap.get(toSnakeCase(relationshipConfig.slug))
// parent to relationship join table
relationshipFields = relationshipConfig.fields
@@ -518,13 +499,13 @@ export const getTableColumnFromPath = ({
}
}
} else if (newCollectionPath === 'value') {
const tableColumnsNames = field.relationTo.map(
(relationTo) =>
`"${aliasRelationshipTableName}"."${getTableName({
adapter,
config: adapter.payload.collections[relationTo].config,
})}_id"`,
)
const tableColumnsNames = field.relationTo.map((relationTo) => {
const relationTableName = adapter.tableNameMap.get(
toSnakeCase(adapter.payload.collections[relationTo].config.slug),
)
return `"${aliasRelationshipTableName}"."${relationTableName}_id"`
})
return {
constraints,
field,

View File

@@ -1,27 +1,30 @@
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import type { PayloadRequestWithData, 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.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const queryDrafts: QueryDrafts = async function queryDrafts({
collection,
limit,
locale,
page = 1,
pagination,
req = {} as PayloadRequest,
sort,
where,
}) {
export const queryDrafts: QueryDrafts = async function queryDrafts(
this: PostgresAdapter,
{
collection,
limit,
locale,
page = 1,
pagination,
req = {} as PayloadRequestWithData,
sort,
where,
},
) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const tableName = this.tableNameMap.get(
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
)
const fields = buildVersionCollectionFields(collectionConfig)
const combinedWhere = combineQueries({ latest: { equals: true } }, where)

View File

@@ -1,6 +1,7 @@
/* eslint-disable no-param-reassign */
import type { Relation } from 'drizzle-orm'
import type {
ForeignKeyBuilder,
IndexBuilder,
PgColumnBuilder,
PgTableWithColumns,
@@ -9,20 +10,34 @@ import type {
import type { Field } from 'payload/types'
import { relations } from 'drizzle-orm'
import { index, integer, numeric, serial, timestamp, unique, varchar } from 'drizzle-orm/pg-core'
import { fieldAffectsData } from 'payload/types'
import {
foreignKey,
index,
integer,
numeric,
serial,
timestamp,
unique,
varchar,
} from 'drizzle-orm/pg-core'
import toSnakeCase from 'to-snake-case'
import type { GenericColumns, GenericTable, IDType, PostgresAdapter } from '../types.js'
import { getTableName } from './getTableName.js'
import { createTableName } from './createTableName.js'
import { parentIDColumnMap } from './parentIDColumnMap.js'
import { setColumnID } from './setColumnID.js'
import { traverseFields } from './traverseFields.js'
export type BaseExtraConfig = Record<
string,
(cols: GenericColumns) => ForeignKeyBuilder | IndexBuilder | UniqueConstraintBuilder
>
type Args = {
adapter: PostgresAdapter
baseColumns?: Record<string, PgColumnBuilder>
baseExtraConfig?: Record<string, (cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder>
baseExtraConfig?: BaseExtraConfig
buildNumbers?: boolean
buildRelationships?: boolean
buildTexts?: boolean
@@ -134,10 +149,12 @@ export const buildTable = ({
return config
}, {})
return Object.entries(indexes).reduce((acc, [colName, func]) => {
const result = Object.entries(indexes).reduce((acc, [colName, func]) => {
acc[colName] = func(cols)
return acc
}, extraConfig)
return result
})
adapter.tables[tableName] = table
@@ -146,9 +163,7 @@ 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')
.references(() => table.id, { onDelete: 'cascade' })
.notNull()
localesColumns._parentID = parentIDColumnMap[idColType]('_parent_id').notNull()
localesTable = adapter.pgSchema.table(localeTableName, localesColumns, (cols) => {
return Object.entries(localesIndexes).reduce(
@@ -161,6 +176,11 @@ export const buildTable = ({
cols._locale,
cols._parentID,
),
_parentIdFk: foreignKey({
name: `${localeTableName}_parent_id_fk`,
columns: [cols._parentID],
foreignColumns: [table.id],
}).onDelete('cascade'),
},
)
})
@@ -182,9 +202,7 @@ export const buildTable = ({
const columns: Record<string, PgColumnBuilder> = {
id: serial('id').primaryKey(),
order: integer('order').notNull(),
parent: parentIDColumnMap[idColType]('parent_id')
.references(() => table.id, { onDelete: 'cascade' })
.notNull(),
parent: parentIDColumnMap[idColType]('parent_id').notNull(),
path: varchar('path').notNull(),
text: varchar('text'),
}
@@ -194,19 +212,24 @@ export const buildTable = ({
}
textsTable = adapter.pgSchema.table(textsTableName, columns, (cols) => {
const indexes: Record<string, IndexBuilder> = {
const config: Record<string, ForeignKeyBuilder | 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') {
indexes.text_idx = index(`${textsTableName}_text_idx`).on(cols.text)
config.text_idx = index(`${textsTableName}_text_idx`).on(cols.text)
}
if (hasLocalizedManyTextField) {
indexes.localeParent = index(`${textsTableName}_locale_parent`).on(cols.locale, cols.parent)
config.localeParent = index(`${textsTableName}_locale_parent`).on(cols.locale, cols.parent)
}
return indexes
return config
})
adapter.tables[textsTableName] = textsTable
@@ -227,9 +250,7 @@ export const buildTable = ({
id: serial('id').primaryKey(),
number: numeric('number'),
order: integer('order').notNull(),
parent: parentIDColumnMap[idColType]('parent_id')
.references(() => table.id, { onDelete: 'cascade' })
.notNull(),
parent: parentIDColumnMap[idColType]('parent_id').notNull(),
path: varchar('path').notNull(),
}
@@ -238,22 +259,27 @@ export const buildTable = ({
}
numbersTable = adapter.pgSchema.table(numbersTableName, columns, (cols) => {
const indexes: Record<string, IndexBuilder> = {
const config: Record<string, ForeignKeyBuilder | 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') {
indexes.numberIdx = index(`${numbersTableName}_number_idx`).on(cols.number)
config.numberIdx = index(`${numbersTableName}_number_idx`).on(cols.number)
}
if (hasLocalizedManyNumberField) {
indexes.localeParent = index(`${numbersTableName}_locale_parent`).on(
config.localeParent = index(`${numbersTableName}_locale_parent`).on(
cols.locale,
cols.parent,
)
}
return indexes
return config
})
adapter.tables[numbersTableName] = numbersTable
@@ -273,9 +299,7 @@ export const buildTable = ({
const relationshipColumns: Record<string, PgColumnBuilder> = {
id: serial('id').primaryKey(),
order: integer('order'),
parent: parentIDColumnMap[idColType]('parent_id')
.references(() => table.id, { onDelete: 'cascade' })
.notNull(),
parent: parentIDColumnMap[idColType]('parent_id').notNull(),
path: varchar('path').notNull(),
}
@@ -283,9 +307,13 @@ 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 = getTableName({
const formattedRelationTo = createTableName({
adapter,
config: relationshipConfig,
throwValidationError: true,
@@ -300,20 +328,38 @@ export const buildTable = ({
relationshipColumns[`${relationTo}ID`] = parentIDColumnMap[colType](
`${formattedRelationTo}_id`,
).references(() => adapter.tables[formattedRelationTo].id, { onDelete: 'cascade' })
})
)
const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`
relationExtraConfig[`${relationTo}IdFk`] = (cols) =>
foreignKey({
name: `${relationshipsTableName}_${toSnakeCase(relationTo)}_fk`,
columns: [cols[`${relationTo}ID`]],
foreignColumns: [adapter.tables[formattedRelationTo].id],
}).onDelete('cascade')
})
relationshipsTable = adapter.pgSchema.table(
relationshipsTableName,
relationshipColumns,
(cols) => {
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),
}
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),
},
)
if (hasLocalizedRelationshipField) {
result.localeIdx = index(`${relationshipsTableName}_locale_idx`).on(cols.locale)
@@ -335,7 +381,7 @@ export const buildTable = ({
}
relationships.forEach((relationTo) => {
const relatedTableName = getTableName({
const relatedTableName = createTableName({
adapter,
config: adapter.payload.collections[relationTo].config,
throwValidationError: true,

View File

@@ -14,53 +14,59 @@ 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, should only be used on the base collection to duplicate suffixing */
/** Adds the versions suffix to the default table name - should only be used on the base collection to avoid 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 getTableName = ({
export const createTableName = ({
adapter,
config: { name, slug },
config,
locales = false,
parentTableName,
prefix = '',
relationships = false,
target = 'dbName',
throwValidationError = false,
versions = false,
versionsCustomName = false,
}: Args): string => {
let result: string
let custom = config[target]
let customNameDefinition = config[target]
if (!custom && target === 'enumName') {
custom = config['dbName']
let defaultTableName = `${prefix}${toSnakeCase(name ?? slug)}`
if (versions) defaultTableName = `_${defaultTableName}${adapter.versionsSuffix}`
let customTableNameResult: string
if (!customNameDefinition && target === 'enumName') {
customNameDefinition = config['dbName']
}
if (custom) {
result = typeof custom === 'function' ? custom({ tableName: parentTableName }) : custom
} else {
result = `${prefix}${toSnakeCase(name ?? slug)}`
if (customNameDefinition) {
customTableNameResult =
typeof customNameDefinition === 'function'
? customNameDefinition({ tableName: parentTableName })
: customNameDefinition
if (versionsCustomName)
customTableNameResult = `_${customTableNameResult}${adapter.versionsSuffix}`
}
if (locales) result = `${result}${adapter.localesSuffix}`
if (versions) result = `_${result}${adapter.versionsSuffix}`
if (relationships) result = `${result}${adapter.relationshipsSuffix}`
const result = customTableNameResult || defaultTableName
adapter.tableNameMap.set(defaultTableName, result)
if (!throwValidationError) {
return result
@@ -71,5 +77,6 @@ export const getTableName = ({
`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, UniqueConstraintBuilder } from 'drizzle-orm/pg-core'
import type { IndexBuilder, PgColumnBuilder } from 'drizzle-orm/pg-core'
import type { Field, TabAsField } from 'payload/types'
import { relations } from 'drizzle-orm'
@@ -9,6 +9,7 @@ import {
PgUUIDBuilder,
PgVarcharBuilder,
boolean,
foreignKey,
index,
integer,
jsonb,
@@ -23,11 +24,12 @@ import { fieldAffectsData, optionIsObject } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { GenericColumns, IDType, PostgresAdapter } from '../types.js'
import type { BaseExtraConfig } from './build.js'
import { hasLocalesTable } from '../utilities/hasLocalesTable.js'
import { buildTable } from './build.js'
import { createIndex } from './createIndex.js'
import { getTableName } from './getTableName.js'
import { createTableName } from './createTableName.js'
import { idToUUID } from './idToUUID.js'
import { parentIDColumnMap } from './parentIDColumnMap.js'
import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdentical.js'
@@ -221,14 +223,13 @@ export const traverseFields = ({
case 'radio':
case 'select': {
const enumName = getTableName({
const enumName = createTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `enum_${newTableName}_`,
target: 'enumName',
throwValidationError,
versions,
})
adapter.enums[enumName] = pgEnum(
@@ -243,27 +244,27 @@ export const traverseFields = ({
)
if (field.type === 'select' && field.hasMany) {
const selectTableName = getTableName({
const selectTableName = createTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
versions,
})
const baseColumns: Record<string, PgColumnBuilder> = {
order: integer('order').notNull(),
parent: parentIDColumnMap[parentIDColType]('parent_id')
.references(() => adapter.tables[parentTableName].id, { onDelete: 'cascade' })
.notNull(),
parent: parentIDColumnMap[parentIDColType]('parent_id').notNull(),
value: adapter.enums[enumName]('value'),
}
const baseExtraConfig: Record<
string,
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
> = {
const baseExtraConfig: BaseExtraConfig = {
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),
}
@@ -316,25 +317,28 @@ export const traverseFields = ({
case 'array': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const arrayTableName = getTableName({
const arrayTableName = createTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
versionsCustomName: versions,
})
const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
_parentID: parentIDColumnMap[parentIDColType]('_parent_id')
.references(() => adapter.tables[parentTableName].id, { onDelete: 'cascade' })
.notNull(),
_parentID: parentIDColumnMap[parentIDColType]('_parent_id').notNull(),
}
const baseExtraConfig: Record<
string,
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
> = {
const baseExtraConfig: BaseExtraConfig = {
_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),
}
@@ -402,28 +406,30 @@ export const traverseFields = ({
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
field.blocks.forEach((block) => {
const blockTableName = getTableName({
const blockTableName = createTableName({
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')
.references(() => adapter.tables[rootTableName].id, { onDelete: 'cascade' })
.notNull(),
_parentID: parentIDColumnMap[rootTableIDColType]('_parent_id').notNull(),
_path: text('_path').notNull(),
}
const baseExtraConfig: Record<
string,
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
> = {
const baseExtraConfig: BaseExtraConfig = {
_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),
}
@@ -496,7 +502,6 @@ export const traverseFields = ({
tableLocales: adapter.tables[`${blockTableName}${adapter.localesSuffix}`],
})
}
adapter.blockTableNames[`${rootTableName}.${toSnakeCase(block.slug)}`] = blockTableName
rootRelationsToBuild.set(`_blocks_${block.slug}`, blockTableName)
})
@@ -659,7 +664,7 @@ export const traverseFields = ({
indexes,
localesColumns,
localesIndexes,
newTableName: parentTableName,
newTableName,
parentTableName,
relationsToBuild,
relationships,

View File

@@ -28,7 +28,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 +36,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)
@@ -51,7 +51,7 @@ const getFlattenedFieldNames = (
return [
...fieldsToUse,
{
name: `${fieldPrefix?.replace('.', '_') || ''}${field.name}`,
name: `${fieldPrefix}${field.name}`,
localized: field.localized,
},
]
@@ -85,7 +85,11 @@ export const validateExistingBlockIsIdentical = ({
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 ${
typeof missingField === 'string' ? missingField : missingField.name
}, while the other block does not.`,
)
}

View File

@@ -45,6 +45,7 @@ export const transformArray = ({
texts,
}: Args) => {
const newRows: ArrayRowToInsert[] = []
const hasUUID = adapter.tables[arrayTableName]._uuid
if (isArrayOfRows(data)) {

View File

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

View File

@@ -94,7 +94,7 @@ export const traverseFields = ({
}
if (field.type === 'array') {
const arrayTableName = `${parentTableName}_${columnName}`
const arrayTableName = adapter.tableNameMap.get(`${parentTableName}_${columnName}`)
if (!arrays[arrayTableName]) arrays[arrayTableName] = []
@@ -458,7 +458,7 @@ export const traverseFields = ({
}
if (field.type === 'select' && field.hasMany) {
const selectTableName = `${parentTableName}_${columnName}`
const selectTableName = adapter.tableNameMap.get(`${parentTableName}_${columnName}`)
if (!selects[selectTableName]) selects[selectTableName] = []
if (field.localized) {

View File

@@ -17,7 +17,7 @@ import type {
import type { PgTableFn } from 'drizzle-orm/pg-core/table'
import type { Payload } from 'payload'
import type { BaseDatabaseAdapter } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import type { Pool, PoolConfig } from 'pg'
export type DrizzleDB = NodePgDatabase<Record<string, unknown>>
@@ -61,10 +61,6 @@ 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>
/**
@@ -90,6 +86,7 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
resolve: () => Promise<void>
}
}
tableNameMap: Map<string, string>
tables: Record<string, GenericTable | PgTableWithColumns<any>>
versionsSuffix?: string
}
@@ -98,8 +95,8 @@ export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar'
export type PostgresAdapterResult = (args: { payload: Payload }) => PostgresAdapter
export type MigrateUpArgs = { payload: Payload; req?: Partial<PayloadRequest> }
export type MigrateDownArgs = { payload: Payload; req?: Partial<PayloadRequest> }
export type MigrateUpArgs = { payload: Payload; req?: Partial<PayloadRequestWithData> }
export type MigrateDownArgs = { payload: Payload; req?: Partial<PayloadRequestWithData> }
declare module 'payload' {
export interface DatabaseAdapter

View File

@@ -1,10 +1,11 @@
import type { UpdateOne } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export const updateOne: UpdateOne = async function updateOne(
@@ -13,10 +14,7 @@ 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 = getTableName({
adapter: this,
config: collection,
})
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
const whereToUse = whereArg || { id: { equals: id } }
let idToUpdate = id

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
import type { TypeWithVersion, UpdateVersionArgs } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig, TypeWithID } from 'payload/types'
import type { PayloadRequestWithData, SanitizedCollectionConfig, TypeWithID } from 'payload/types'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function updateVersion<T extends TypeWithID>(
@@ -15,7 +15,7 @@ export async function updateVersion<T extends TypeWithID>(
id,
collection,
locale,
req = {} as PayloadRequest,
req = {} as PayloadRequestWithData,
versionData,
where: whereArg,
}: UpdateVersionArgs<T>,
@@ -23,11 +23,10 @@ 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 = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const tableName = this.tableNameMap.get(
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
)
const fields = buildVersionCollectionFields(collectionConfig)
const { where } = await buildQuery({

View File

@@ -137,7 +137,7 @@ export const upsertRow = async <T extends TypeWithID>({
// //////////////////////////////////
if (localesToInsert.length > 0) {
const localeTable = adapter.tables[`${tableName}_locales`]
const localeTable = adapter.tables[`${tableName}${adapter.localesSuffix}`]
if (operation === 'update') {
await db.delete(localeTable).where(eq(localeTable._parentID, insertedRow.id))
@@ -150,7 +150,7 @@ export const upsertRow = async <T extends TypeWithID>({
// INSERT RELATIONSHIPS
// //////////////////////////////////
const relationshipsTableName = `${tableName}_rels`
const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`
if (operation === 'update') {
await deleteExistingRowsByPath({
@@ -223,15 +223,16 @@ export const upsertRow = async <T extends TypeWithID>({
if (operation === 'update') {
for (const blockName of rowToInsert.blocksToDelete) {
const blockTableName = `${tableName}_blocks_${blockName}`
const blockTableName = adapter.tableNameMap.get(`${tableName}_blocks_${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}`)
insertedBlockRows[blockName] = await db
.insert(adapter.tables[`${tableName}_blocks_${blockName}`])
.insert(adapter.tables[blockTableName])
.values(blockRows.map(({ row }) => row))
.returning()
@@ -258,7 +259,7 @@ export const upsertRow = async <T extends TypeWithID>({
if (blockLocaleRowsToInsert.length > 0) {
await db
.insert(adapter.tables[`${tableName}_blocks_${blockName}_locales`])
.insert(adapter.tables[`${blockTableName}${adapter.localesSuffix}`])
.values(blockLocaleRowsToInsert)
.returning()
}

View File

@@ -1,5 +1,5 @@
import type { SQL } from 'drizzle-orm'
import type { Field, PayloadRequest } from 'payload/types'
import type { Field, PayloadRequestWithData } from 'payload/types'
import type { DrizzleDB, GenericColumn, PostgresAdapter } from '../types.js'
@@ -9,7 +9,7 @@ type BaseArgs = {
db: DrizzleDB
fields: Field[]
path?: string
req: PayloadRequest
req: PayloadRequestWithData
tableName: string
}

View File

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

View File

@@ -0,0 +1,7 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
}

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2018-2022 Payload CMS, LLC <info@payloadcms.com>
Portions Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1 @@
# Nodemailer Email Adapter

View File

@@ -0,0 +1,59 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.0.0-beta.18",
"description": "Payload Nodemailer Email Adapter",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/email-nodemailer"
},
"license": "MIT",
"homepage": "https://payloadcms.com",
"author": "Payload CMS, Inc.",
"main": "./src/index.ts",
"types": "./src/index.ts",
"type": "module",
"scripts": {
"build": "pnpm build:swc && pnpm build:types",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"build:clean": "find . \\( -type d \\( -name build -o -name dist -o -name .cache \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} + && pnpm build",
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"dependencies": {
"nodemailer": "6.9.10"
},
"peerDependencies": {
"payload": "workspace:*"
},
"exports": {
".": {
"import": "./src/index.ts",
"require": "./src/index.ts",
"types": "./src/index.ts"
}
},
"publishConfig": {
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"
},
"engines": {
"node": ">=18.20.2"
},
"files": [
"dist"
],
"devDependencies": {
"payload": "workspace:*",
"@types/nodemailer": "6.4.14"
}
}

View File

@@ -0,0 +1,123 @@
/* eslint-disable no-console */
import type { Transporter } from 'nodemailer'
import type SMTPConnection from 'nodemailer/lib/smtp-connection'
import type { EmailAdapter } from 'payload/config'
import nodemailer from 'nodemailer'
import { InvalidConfiguration } from 'payload/errors'
export type NodemailerAdapterArgs = {
defaultFromAddress: string
defaultFromName: string
skipVerify?: boolean
transport?: Transporter
transportOptions?: SMTPConnection.Options
}
type NodemailerAdapter = EmailAdapter<unknown>
/**
* Creates an email adapter using nodemailer
*
* If no email configuration is provided, an ethereal email test account is returned
*/
export const nodemailerAdapter = async (
args?: NodemailerAdapterArgs,
): Promise<NodemailerAdapter> => {
const { defaultFromAddress, defaultFromName, transport } = await buildEmail(args)
const adapter: NodemailerAdapter = () => ({
defaultFromAddress,
defaultFromName,
sendEmail: async (message) => {
return await transport.sendMail({
from: `${defaultFromName} <${defaultFromAddress}>`,
...message,
})
},
})
return adapter
}
async function buildEmail(emailConfig?: NodemailerAdapterArgs): Promise<{
defaultFromAddress: string
defaultFromName: string
transport: Transporter
}> {
if (!emailConfig) {
const transport = await createMockAccount(emailConfig)
if (!transport) throw new InvalidConfiguration('Unable to create Nodemailer test account.')
return {
defaultFromAddress: 'info@payloadcms.com',
defaultFromName: 'Payload',
transport,
}
}
// Create or extract transport
let transport: Transporter
if ('transport' in emailConfig && emailConfig.transport) {
;({ transport } = emailConfig)
} else if ('transportOptions' in emailConfig && emailConfig.transportOptions) {
transport = nodemailer.createTransport(emailConfig.transportOptions)
} else {
transport = await createMockAccount(emailConfig)
}
if (emailConfig.skipVerify !== false) {
await verifyTransport(transport)
}
return {
defaultFromAddress: emailConfig.defaultFromAddress,
defaultFromName: emailConfig.defaultFromName,
transport,
}
}
async function verifyTransport(transport: Transporter) {
try {
await transport.verify()
} catch (err: unknown) {
console.error({ err, msg: 'Error verifying Nodemailer transport.' })
}
}
/**
* Use ethereal.email to create a mock email account
*/
async function createMockAccount(emailConfig?: NodemailerAdapterArgs) {
try {
const etherealAccount = await nodemailer.createTestAccount()
const smtpOptions = {
...(emailConfig || {}),
auth: {
pass: etherealAccount.pass,
user: etherealAccount.user,
},
fromAddress: emailConfig?.defaultFromAddress,
fromName: emailConfig?.defaultFromName,
host: 'smtp.ethereal.email',
port: 587,
secure: false,
}
const transport = nodemailer.createTransport(smtpOptions)
const { pass, user, web } = etherealAccount
console.info('E-mail configured with ethereal.email test account. ')
console.info(`Log into mock email provider at ${web}`)
console.info(`Mock email account username: ${user}`)
console.info(`Mock email account password: ${pass}`)
return transport
} catch (err: unknown) {
if (err instanceof Error) {
console.error({ err, msg: 'There was a problem setting up the mock email handler' })
throw new InvalidConfiguration(
`Unable to create Nodemailer test account. Error: ${err.message}`,
)
}
throw new InvalidConfiguration('Unable to create Nodemailer test account.')
}
}

View File

@@ -0,0 +1,19 @@
{
"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. */,
"strict": true,
},
"exclude": [
"dist",
"node_modules",
],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
"references": [
{ "path": "../payload" },
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.0.0-beta.11",
"version": "3.0.0-beta.18",
"main": "./src/index.ts",
"types": "./src/index.d.ts",
"type": "module",
@@ -27,20 +27,17 @@
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/pluralize": "^0.0.33",
"graphql-http": "^1.22.0",
"payload": "workspace:*",
"ts-essentials": "7.0.3"
},
"dependencies": {
"graphql": "16.8.1",
"graphql-http": "^1.22.0",
"graphql-playground-html": "1.6.30",
"graphql-query-complexity": "0.12.0",
"graphql-scalars": "1.22.2",
"graphql-type-json": "0.3.2",
"pluralize": "8.0.0"
},
"peerDependencies": {
"payload": "workspace:*"
"payload": "workspace:*",
"graphql": "^16.8.1"
},
"publishConfig": {
"main": "./dist/index.js",

View File

@@ -4,12 +4,12 @@ import type { GraphQLInfo } from 'payload/config'
import type { SanitizedConfig } from 'payload/types'
import * as GraphQL from 'graphql'
import {
createComplexityRule,
fieldExtensionsEstimator,
simpleEstimator,
} from 'graphql-query-complexity'
} from './packages/graphql-query-complexity/index.js'
import accessResolver from './resolvers/auth/access.js'
import buildFallbackLocaleInputType from './schema/buildFallbackLocaleInputType.js'
import buildLocaleInputType from './schema/buildLocaleInputType.js'
@@ -18,10 +18,10 @@ import initCollections from './schema/initCollections.js'
import initGlobals from './schema/initGlobals.js'
import { wrapCustomFields } from './utilities/wrapCustomResolver.js'
export async function configToSchema(config: SanitizedConfig): Promise<{
export function configToSchema(config: SanitizedConfig): {
schema: GraphQL.GraphQLSchema
validationRules: (args: OperationArgs<any>) => GraphQL.ValidationRule[]
}> {
} {
const collections = config.collections.reduce((acc, collection) => {
acc[collection.slug] = {
config: collection,

View File

@@ -0,0 +1,455 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
/**
* Created by Ivo Meißner on 28.07.17.
*/
import type {
DocumentNode,
FieldNode,
FragmentDefinitionNode,
FragmentSpreadNode,
GraphQLCompositeType,
GraphQLDirective,
GraphQLField,
GraphQLFieldMap,
GraphQLNamedType,
GraphQLSchema,
GraphQLUnionType,
InlineFragmentNode,
OperationDefinitionNode} from 'graphql';
import {
GraphQLError,
GraphQLInterfaceType,
GraphQLObjectType,
Kind,
TypeInfo,
ValidationContext,
getNamedType,
isAbstractType,
isCompositeType,
visit,
visitWithTypeInfo,
} from 'graphql'
import {
getArgumentValues,
getDirectiveValues,
getVariableValues,
} from 'graphql/execution/values.js'
export type ComplexityEstimatorArgs = {
args: { [key: string]: any }
childComplexity: number
context?: Record<string, any>
field: GraphQLField<any, any>
node: FieldNode
type: GraphQLCompositeType
}
export type ComplexityEstimator = (options: ComplexityEstimatorArgs) => number | void
// Complexity can be anything that is supported by the configured estimators
export type Complexity = any
// Map of complexities for possible types (of Union, Interface types)
type ComplexityMap = {
[typeName: string]: number
}
export interface QueryComplexityOptions {
// Pass request context to the estimators via estimationContext
context?: Record<string, any>
// The query variables. This is needed because the variables are not available
// Optional function to create a custom error
createError?: (max: number, actual: number) => GraphQLError
// An array of complexity estimators to use for estimating the complexity
estimators: Array<ComplexityEstimator>
// Optional callback function to retrieve the determined query complexity
// Will be invoked whether the query is rejected or not
// The maximum allowed query complexity, queries above this threshold will be rejected
maximumComplexity: number
// This can be used for logging or to implement rate limiting
onComplete?: (complexity: number) => void
// specify operation name only when pass multi-operation documents
operationName?: string
// in the visitor of the graphql-js library
variables?: Record<string, any>
}
function queryComplexityMessage(max: number, actual: number): string {
return `The query exceeds the maximum complexity of ${max}. ` + `Actual complexity is ${actual}`
}
export function getComplexity(options: {
context?: Record<string, any>
estimators: ComplexityEstimator[]
operationName?: string
query: DocumentNode
schema: GraphQLSchema
variables?: Record<string, any>
}): number {
const typeInfo = new TypeInfo(options.schema)
const errors: GraphQLError[] = []
const context = new ValidationContext(options.schema, options.query, typeInfo, (error) =>
errors.push(error),
)
const visitor = new QueryComplexity(context, {
// Maximum complexity does not matter since we're only interested in the calculated complexity.
context: options.context,
estimators: options.estimators,
maximumComplexity: Infinity,
operationName: options.operationName,
variables: options.variables,
})
visit(options.query, visitWithTypeInfo(typeInfo, visitor))
// Throw first error if any
if (errors.length) {
throw errors.pop()
}
return visitor.complexity
}
export default class QueryComplexity {
OperationDefinition: Record<string, any>
complexity: number
context: ValidationContext
estimators: Array<ComplexityEstimator>
includeDirectiveDef: GraphQLDirective
options: QueryComplexityOptions
requestContext?: Record<string, any>
skipDirectiveDef: GraphQLDirective
variableValues: Record<string, any>
constructor(context: ValidationContext, options: QueryComplexityOptions) {
if (!(typeof options.maximumComplexity === 'number' && options.maximumComplexity > 0)) {
throw new Error('Maximum query complexity must be a positive number')
}
this.context = context
this.complexity = 0
this.options = options
this.includeDirectiveDef = this.context.getSchema().getDirective('include')
this.skipDirectiveDef = this.context.getSchema().getDirective('skip')
this.estimators = options.estimators
this.variableValues = {}
this.requestContext = options.context
this.OperationDefinition = {
enter: this.onOperationDefinitionEnter,
leave: this.onOperationDefinitionLeave,
}
}
createError(): GraphQLError {
if (typeof this.options.createError === 'function') {
return this.options.createError(this.options.maximumComplexity, this.complexity)
}
return new GraphQLError(queryComplexityMessage(this.options.maximumComplexity, this.complexity))
}
nodeComplexity(
node: FieldNode | FragmentDefinitionNode | InlineFragmentNode | OperationDefinitionNode,
typeDef: GraphQLInterfaceType | GraphQLObjectType | GraphQLUnionType,
): number {
if (node.selectionSet) {
let fields: GraphQLFieldMap<any, any> = {}
if (typeDef instanceof GraphQLObjectType || typeDef instanceof GraphQLInterfaceType) {
fields = typeDef.getFields()
}
// Determine all possible types of the current node
let possibleTypeNames: string[]
if (isAbstractType(typeDef)) {
possibleTypeNames = this.context
.getSchema()
.getPossibleTypes(typeDef)
.map((t) => t.name)
} else {
possibleTypeNames = [typeDef.name]
}
// Collect complexities for all possible types individually
const selectionSetComplexities: ComplexityMap = node.selectionSet.selections.reduce(
(
complexities: ComplexityMap,
childNode: FieldNode | FragmentSpreadNode | InlineFragmentNode,
): ComplexityMap => {
// let nodeComplexity = 0;
let innerComplexities = complexities
let includeNode = true
let skipNode = false
for (const directive of childNode.directives ?? []) {
const directiveName = directive.name.value
switch (directiveName) {
case 'include': {
const values = getDirectiveValues(
this.includeDirectiveDef,
childNode,
this.variableValues || {},
)
if (typeof values.if === 'boolean') {
includeNode = values.if
}
break
}
case 'skip': {
const values = getDirectiveValues(
this.skipDirectiveDef,
childNode,
this.variableValues || {},
)
if (typeof values.if === 'boolean') {
skipNode = values.if
}
break
}
}
}
if (!includeNode || skipNode) {
return complexities
}
switch (childNode.kind) {
case Kind.FIELD: {
const field = fields[childNode.name.value]
// Invalid field, should be caught by other validation rules
if (!field) {
break
}
const fieldType = getNamedType(field.type)
// Get arguments
let args: { [key: string]: any }
try {
args = getArgumentValues(field, childNode, this.variableValues || {})
} catch (e) {
this.context.reportError(e)
return complexities
}
// Check if we have child complexity
let childComplexity = 0
if (isCompositeType(fieldType)) {
childComplexity = this.nodeComplexity(childNode, fieldType)
}
// Run estimators one after another and return first valid complexity
// score
const estimatorArgs: ComplexityEstimatorArgs = {
type: typeDef,
args,
childComplexity,
context: this.requestContext,
field,
node: childNode,
}
const validScore = this.estimators.find((estimator) => {
const tmpComplexity = estimator(estimatorArgs)
if (typeof tmpComplexity === 'number' && !isNaN(tmpComplexity)) {
innerComplexities = addComplexities(
tmpComplexity,
complexities,
possibleTypeNames,
)
return true
}
return false
})
if (!validScore) {
this.context.reportError(
new GraphQLError(
`No complexity could be calculated for field ${typeDef.name}.${field.name}. ` +
'At least one complexity estimator has to return a complexity score.',
),
)
return complexities
}
break
}
case Kind.FRAGMENT_SPREAD: {
const fragment = this.context.getFragment(childNode.name.value)
// Unknown fragment, should be caught by other validation rules
if (!fragment) {
break
}
const fragmentType = this.context
.getSchema()
.getType(fragment.typeCondition.name.value)
// Invalid fragment type, ignore. Should be caught by other validation rules
if (!isCompositeType(fragmentType)) {
break
}
const nodeComplexity = this.nodeComplexity(fragment, fragmentType)
if (isAbstractType(fragmentType)) {
// Add fragment complexity for all possible types
innerComplexities = addComplexities(
nodeComplexity,
complexities,
this.context
.getSchema()
.getPossibleTypes(fragmentType)
.map((t) => t.name),
)
} else {
// Add complexity for object type
innerComplexities = addComplexities(nodeComplexity, complexities, [
fragmentType.name,
])
}
break
}
case Kind.INLINE_FRAGMENT: {
let inlineFragmentType: GraphQLNamedType = typeDef
if (childNode.typeCondition && childNode.typeCondition.name) {
inlineFragmentType = this.context
.getSchema()
.getType(childNode.typeCondition.name.value)
if (!isCompositeType(inlineFragmentType)) {
break
}
}
const nodeComplexity = this.nodeComplexity(childNode, inlineFragmentType)
if (isAbstractType(inlineFragmentType)) {
// Add fragment complexity for all possible types
innerComplexities = addComplexities(
nodeComplexity,
complexities,
this.context
.getSchema()
.getPossibleTypes(inlineFragmentType)
.map((t) => t.name),
)
} else {
// Add complexity for object type
innerComplexities = addComplexities(nodeComplexity, complexities, [
inlineFragmentType.name,
])
}
break
}
default: {
innerComplexities = addComplexities(
this.nodeComplexity(childNode, typeDef),
complexities,
possibleTypeNames,
)
break
}
}
return innerComplexities
},
{},
)
// Only return max complexity of all possible types
if (!selectionSetComplexities) {
return NaN
}
return Math.max(...Object.values(selectionSetComplexities), 0)
}
return 0
}
onOperationDefinitionEnter(operation: OperationDefinitionNode): void {
if (
typeof this.options.operationName === 'string' &&
this.options.operationName !== operation.name.value
) {
return
}
// Get variable values from variables that are passed from options, merged
// with default values defined in the operation
const { coerced, errors } = getVariableValues(
this.context.getSchema(),
// We have to create a new array here because input argument is not readonly in graphql ~14.6.0
operation.variableDefinitions ? [...operation.variableDefinitions] : [],
this.options.variables ?? {},
)
if (errors && errors.length) {
// We have input validation errors, report errors and abort
errors.forEach((error) => this.context.reportError(error))
return
}
this.variableValues = coerced
switch (operation.operation) {
case 'query':
this.complexity += this.nodeComplexity(operation, this.context.getSchema().getQueryType())
break
case 'mutation':
this.complexity += this.nodeComplexity(
operation,
this.context.getSchema().getMutationType(),
)
break
case 'subscription':
this.complexity += this.nodeComplexity(
operation,
this.context.getSchema().getSubscriptionType(),
)
break
default:
throw new Error(
`Query complexity could not be calculated for operation of type ${operation.operation}`,
)
}
}
onOperationDefinitionLeave(operation: OperationDefinitionNode): GraphQLError | void {
if (
typeof this.options.operationName === 'string' &&
this.options.operationName !== operation.name.value
) {
return
}
if (this.options.onComplete) {
this.options.onComplete(this.complexity)
}
if (this.complexity > this.options.maximumComplexity) {
return this.context.reportError(this.createError())
}
}
}
/**
* Adds a complexity to the complexity map for all possible types
* @param complexity
* @param complexityMap
* @param possibleTypes
*/
function addComplexities(
complexity: number,
complexityMap: ComplexityMap,
possibleTypes: string[],
): ComplexityMap {
for (const type of possibleTypes) {
if (Object.prototype.hasOwnProperty.call(complexityMap, type)) {
complexityMap[type] += complexity
} else {
complexityMap[type] = complexity
}
}
return complexityMap
}

View File

@@ -0,0 +1,13 @@
import type { ValidationContext } from 'graphql'
import type { QueryComplexityOptions } from './QueryComplexity.js'
import QueryComplexity from './QueryComplexity.js'
export function createComplexityRule(
options: QueryComplexityOptions,
): (context: ValidationContext) => QueryComplexity {
return (context: ValidationContext): QueryComplexity => {
return new QueryComplexity(context, options)
}
}

View File

@@ -0,0 +1,14 @@
import type { ComplexityEstimator, ComplexityEstimatorArgs } from '../../QueryComplexity.js'
export const fieldExtensionsEstimator = (): ComplexityEstimator => {
return (args: ComplexityEstimatorArgs): number | void => {
if (args.field.extensions) {
// Calculate complexity score
if (typeof args.field.extensions.complexity === 'number') {
return args.childComplexity + args.field.extensions.complexity
} else if (typeof args.field.extensions.complexity === 'function') {
return args.field.extensions.complexity(args)
}
}
}
}

View File

@@ -0,0 +1,9 @@
import type { ComplexityEstimator, ComplexityEstimatorArgs } from '../../QueryComplexity.js'
export const simpleEstimator = (options?: { defaultComplexity?: number }): ComplexityEstimator => {
const defaultComplexity =
options && typeof options.defaultComplexity === 'number' ? options.defaultComplexity : 1
return (args: ComplexityEstimatorArgs): number | void => {
return defaultComplexity + args.childComplexity
}
}

View File

@@ -0,0 +1,3 @@
export { createComplexityRule } from './createComplexityRule.js'
export { fieldExtensionsEstimator } from './estimators/fieldExtensions/index.js'
export { simpleEstimator } from './estimators/simple/index.js'

View File

@@ -0,0 +1,73 @@
import { GraphQLScalarType } from 'graphql'
import { Kind, print } from 'graphql/language'
function identity(value) {
return value
}
function ensureObject(value) {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new TypeError(`JSONObject cannot represent non-object value: ${value}`)
}
return value
}
function parseObject(typeName, ast, variables) {
const value = Object.create(null)
ast.fields.forEach((field) => {
// eslint-disable-next-line no-use-before-define
value[field.name.value] = parseLiteral(typeName, field.value, variables)
})
return value
}
function parseLiteral(typeName, ast, variables) {
switch (ast.kind) {
case Kind.STRING:
case Kind.BOOLEAN:
return ast.value
case Kind.INT:
case Kind.FLOAT:
return parseFloat(ast.value)
case Kind.OBJECT:
return parseObject(typeName, ast, variables)
case Kind.LIST:
return ast.values.map((n) => parseLiteral(typeName, n, variables))
case Kind.NULL:
return null
case Kind.VARIABLE:
return variables ? variables[ast.name.value] : undefined
default:
throw new TypeError(`${typeName} cannot represent value: ${print(ast)}`)
}
}
// This named export is intended for users of CommonJS. Users of ES modules
// should instead use the default export.
export const GraphQLJSON = new GraphQLScalarType({
name: 'JSON',
description:
'The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).',
parseLiteral: (ast, variables) => parseLiteral('JSON', ast, variables),
parseValue: identity,
serialize: identity,
specifiedByURL: 'http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf',
})
export const GraphQLJSONObject = new GraphQLScalarType({
name: 'JSONObject',
description:
'The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).',
parseLiteral: (ast, variables) => {
if (ast.kind !== Kind.OBJECT) {
throw new TypeError(`JSONObject cannot represent non-object value: ${print(ast)}`)
}
return parseObject('JSONObject', ast, variables)
},
parseValue: ensureObject,
serialize: ensureObject,
specifiedByURL: 'http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf',
})

View File

@@ -0,0 +1,41 @@
import type { PayloadRequestWithData, Where } from 'payload/types'
import type { Collection } from 'payload/types'
import { countOperation } from 'payload/operations'
import { isolateObjectProperty } from 'payload/utilities'
import type { Context } from '../types.js'
export type Resolver = (
_: unknown,
args: {
data: Record<string, unknown>
locale?: string
where?: Where
},
context: {
req: PayloadRequestWithData
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => Promise<{ totalDocs: number }>
export default function countResolver(collection: Collection): Resolver {
return async function resolver(_, args, context: 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(req, 'transactionID'),
where: args.where,
}
const results = await countOperation(options)
return results
}
}

View File

@@ -1,5 +1,5 @@
import type { GeneratedTypes } from 'payload'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import type { Collection } from 'payload/types'
import type { MarkOptional } from 'ts-essentials'
@@ -19,7 +19,7 @@ export type Resolver<TSlug extends keyof GeneratedTypes['collections']> = (
locale?: string
},
context: {
req: PayloadRequest
req: PayloadRequestWithData
},
) => Promise<GeneratedTypes['collections'][TSlug]>

View File

@@ -1,5 +1,5 @@
import type { GeneratedTypes } from 'payload'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import type { Collection } from 'payload/types'
import { deleteByIDOperation } from 'payload/operations'
@@ -14,7 +14,7 @@ export type Resolver<TSlug extends keyof GeneratedTypes['collections']> = (
locale?: string
},
context: {
req: PayloadRequest
req: PayloadRequestWithData
},
) => Promise<GeneratedTypes['collections'][TSlug]>

View File

@@ -1,5 +1,5 @@
import type { CollectionPermission, GlobalPermission } from 'payload/auth'
import type { Collection, PayloadRequest } from 'payload/types'
import type { Collection, PayloadRequestWithData } from 'payload/types'
import { docAccessOperation } from 'payload/operations'
import { isolateObjectProperty } from 'payload/utilities'
@@ -12,7 +12,7 @@ export type Resolver = (
id: number | string
},
context: {
req: PayloadRequest
req: PayloadRequestWithData
},
) => Promise<CollectionPermission | GlobalPermission>

View File

@@ -1,5 +1,5 @@
import type { GeneratedTypes } from 'payload'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import type { Collection } from 'payload/types'
import { duplicateOperation } from 'payload/operations'
@@ -16,7 +16,7 @@ export type Resolver<T> = (
locale?: string
},
context: {
req: PayloadRequest
req: PayloadRequestWithData
},
) => Promise<T>

View File

@@ -1,5 +1,5 @@
import type { PaginatedDocs } from 'payload/database'
import type { PayloadRequest, Where } from 'payload/types'
import type { PayloadRequestWithData, Where } from 'payload/types'
import type { Collection } from 'payload/types'
import { findOperation } from 'payload/operations'
@@ -20,7 +20,7 @@ export type Resolver = (
where?: Where
},
context: {
req: PayloadRequest
req: PayloadRequestWithData
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => Promise<PaginatedDocs<any>>

View File

@@ -1,5 +1,5 @@
import type { GeneratedTypes } from 'payload'
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import type { Collection } from 'payload/types'
import { findByIDOperation } from 'payload/operations'
@@ -16,7 +16,7 @@ export type Resolver<T> = (
locale?: string
},
context: {
req: PayloadRequest
req: PayloadRequestWithData
},
) => Promise<T>

View File

@@ -1,4 +1,4 @@
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import type { Collection, TypeWithID } from 'payload/types'
import type { TypeWithVersion } from 'payload/versions'
@@ -16,7 +16,7 @@ export type Resolver<T extends TypeWithID = any> = (
locale?: string
},
context: {
req: PayloadRequest
req: PayloadRequestWithData
},
) => Promise<TypeWithVersion<T>>

View File

@@ -1,5 +1,5 @@
import type { PaginatedDocs } from 'payload/database'
import type { PayloadRequest, Where } from 'payload/types'
import type { PayloadRequestWithData, Where } from 'payload/types'
import type { Collection } from 'payload/types'
import { findVersionsOperation } from 'payload/operations'
@@ -18,7 +18,7 @@ export type Resolver = (
where: Where
},
context: {
req: PayloadRequest
req: PayloadRequestWithData
},
) => Promise<PaginatedDocs<any>>

View File

@@ -1,4 +1,4 @@
import type { PayloadRequest } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import type { Collection } from 'payload/types'
import { restoreVersionOperation } from 'payload/operations'
@@ -12,7 +12,7 @@ export type Resolver = (
id: number | string
},
context: {
req: PayloadRequest
req: PayloadRequestWithData
},
) => Promise<Document>

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