Compare commits
33 Commits
db-postgre
...
db-postgre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5d6cdae38 | ||
|
|
218f2ead03 | ||
|
|
e9c1222182 | ||
|
|
c8ed6454a7 | ||
|
|
4077598777 | ||
|
|
65d7d54ba3 | ||
|
|
6690c37c4e | ||
|
|
0efc610210 | ||
|
|
cc99c3a619 | ||
|
|
24a8dc7aa3 | ||
|
|
90764efa9a | ||
|
|
d05e3b0411 | ||
|
|
e4bc281fc2 | ||
|
|
9d05b82dc6 | ||
|
|
f2284f3d1b | ||
|
|
1347b6cc36 | ||
|
|
0a56d50334 | ||
|
|
02999a5659 | ||
|
|
365127bee4 | ||
|
|
b67e97aa7f | ||
|
|
61e8ce1743 | ||
|
|
034aa68cd4 | ||
|
|
268e6c485e | ||
|
|
4c1a5dca44 | ||
|
|
a12d1f4755 | ||
|
|
d55be73992 | ||
|
|
b9f236ae50 | ||
|
|
1d38e6d5d5 | ||
|
|
2f3c994cea | ||
|
|
0586f236bb | ||
|
|
d582619ead | ||
|
|
17fc2d13d0 | ||
|
|
800ffd2611 |
70
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
70
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
@@ -2,25 +2,6 @@ name: Bug Report v3
|
||||
description: Create a bug report for Payload v3 (beta)
|
||||
labels: ['status: needs-triage', 'v3']
|
||||
body:
|
||||
- type: input
|
||||
id: reproduction-link
|
||||
attributes:
|
||||
label: Link to reproduction
|
||||
description: Want us to look into your issue faster? Follow the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md) for more information.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment Info
|
||||
description: Paste output from `pnpm payload info` (>= beta.92) _or_ Payload, Node.js, and Next.js versions.
|
||||
render: text
|
||||
placeholder: |
|
||||
Payload:
|
||||
Node.js:
|
||||
Next.js:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
@@ -28,6 +9,16 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: reproduction-link
|
||||
attributes:
|
||||
label: Link to the code that reproduces this issue
|
||||
description: >-
|
||||
Required: Please provide a link to your reproduction. Note, if the URL is invalid (404 or a private repository), we may close the issue.
|
||||
Either use `npx create-payload-app@beta -t blank` or follow the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md) for more information.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
@@ -35,11 +26,44 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: adapters-plugins
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Adapters and Plugins
|
||||
description: What adapters and plugins are you using if relevant? ie. db-mongodb, db-postgres, storage-vercel-blob, etc.
|
||||
label: Which area(s) are affected? (Select all that apply)
|
||||
multiple: true
|
||||
options:
|
||||
- 'Not sure'
|
||||
- 'area: core'
|
||||
- 'area: templates'
|
||||
- 'area: ui'
|
||||
- 'db-mongodb'
|
||||
- 'db-postgres'
|
||||
- 'db-sqlite'
|
||||
- 'db-vercel-postgres'
|
||||
- 'plugin: cloud'
|
||||
- 'plugin: cloud-storage'
|
||||
- 'plugin: form-builder'
|
||||
- 'plugin: nested-docs'
|
||||
- 'plugin: richtext-lexical'
|
||||
- 'plugin: richtext-slate'
|
||||
- 'plugin: search'
|
||||
- 'plugin: sentry'
|
||||
- 'plugin: seo'
|
||||
- 'plugin: stripe'
|
||||
- 'plugin: other'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment Info
|
||||
description: Paste output from `pnpm payload info` (>= beta.92) _or_ Payload, Node.js, and Next.js versions.
|
||||
render: bash
|
||||
placeholder: |
|
||||
Payload:
|
||||
Node.js:
|
||||
Next.js:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
18
.github/comments/invalid-reproduction.md
vendored
Normal file
18
.github/comments/invalid-reproduction.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
We cannot recreate the issue with the provided information. **Please add a reproduction in order for us to be able to investigate.**
|
||||
|
||||
### Why was this issue marked with the `invalid-reproduction` label?
|
||||
|
||||
To be able to investigate, we need access to a reproduction to identify what triggered the issue. We prefer a link to a public GitHub repository created with `create-payload-app@beta -t blank` or a forked/branched version of this repository with tests added (more info in the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)).
|
||||
|
||||
To make sure the issue is resolved as quickly as possible, please make sure that the reproduction is as **minimal** as possible. This means that you should **remove unnecessary code, files, and dependencies** that do not contribute to the issue. Ensure your reproduction does not depend on secrets, 3rd party registries, private dependencies, or any other data that cannot be made public. Avoid a reproduction including a whole monorepo (unless relevant to the issue). The easier it is to reproduce the issue, the quicker we can help.
|
||||
|
||||
Please test your reproduction against the latest version of Payload to make sure your issue has not already been fixed.
|
||||
|
||||
### I added a link, why was it still marked?
|
||||
|
||||
Ensure the link is pointing to a codebase that is accessible (e.g. not a private repository). "[example.com](http://example.com/)", "n/a", "will add later", etc. are not acceptable links -- we need to see a public codebase. See the above section for accepted links.
|
||||
|
||||
### Useful Resources
|
||||
|
||||
- [Reproduction Guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)
|
||||
- [Contributing to Payload](https://www.youtube.com/watch?v=08Qa3ggR9rw)
|
||||
31
.github/dependabot.yml
vendored
31
.github/dependabot.yml
vendored
@@ -31,17 +31,44 @@ updates:
|
||||
labels:
|
||||
- dependencies
|
||||
groups:
|
||||
production:
|
||||
production-deps:
|
||||
dependency-type: production
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
patterns:
|
||||
- '*'
|
||||
dev:
|
||||
exclude-patterns:
|
||||
- 'drizzle*'
|
||||
dev-deps:
|
||||
dependency-type: development
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
patterns:
|
||||
- '*'
|
||||
exclude-patterns:
|
||||
- 'drizzle*'
|
||||
|
||||
# Only bump patch versions for 2.x
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
target-branch: main
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: sunday
|
||||
timezone: America/Detroit
|
||||
time: '06:00'
|
||||
commit-message:
|
||||
prefix: 'chore(deps)'
|
||||
labels:
|
||||
- dependencies
|
||||
groups:
|
||||
production-deps:
|
||||
dependency-type: production
|
||||
update-types:
|
||||
- patch
|
||||
patterns:
|
||||
- '*'
|
||||
exclude-patterns:
|
||||
- 'drizzle*'
|
||||
|
||||
1
.github/workflows/lock-issues.yml
vendored
1
.github/workflows/lock-issues.yml
vendored
@@ -18,6 +18,7 @@ jobs:
|
||||
with:
|
||||
process-only: 'issues'
|
||||
issue-inactive-days: '1'
|
||||
exclude-any-issue-labels: 'status: awaiting-reply'
|
||||
log-output: true
|
||||
issue-comment: >
|
||||
This issue has been automatically locked.
|
||||
|
||||
29
.github/workflows/triage.yml
vendored
Normal file
29
.github/workflows/triage.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: triage
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
name: nissuer
|
||||
if: false # Disable after adjusting scenarios which this should be applied
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: balazsorban44/nissuer@1.10.0
|
||||
with:
|
||||
label-area-prefix: ""
|
||||
label-area-match: "name"
|
||||
label-area-section: 'Which area\(s\) are affected\? \(Select all that apply\)(.*)### Environment Info'
|
||||
reproduction-comment: '.github/comments/invalid-reproduction.md'
|
||||
reproduction-blocklist: 'github.com/\\w*/?$,github.com$'
|
||||
reproduction-link-section: '### Link to the code that reproduces this issue(.*)### Reproduction Steps'
|
||||
reproduction-invalid-label: 'invalid-reproduction'
|
||||
reproduction-issue-labels: 'status: needs-triage,'
|
||||
31
CHANGELOG.md
31
CHANGELOG.md
@@ -1,3 +1,34 @@
|
||||
## [2.30.3](https://github.com/payloadcms/payload/compare/v2.30.2...v2.30.3) (2024-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **db-postgres:** migrate:create errors with previous schemas ([#8786](https://github.com/payloadcms/payload/issues/8786)) ([e9c1222](https://github.com/payloadcms/payload/commit/e9c12221824a9a180991722135d22ff91d07ef11))
|
||||
* duplicate with select hasMany fields ([#8734](https://github.com/payloadcms/payload/issues/8734)) ([c8ed645](https://github.com/payloadcms/payload/commit/c8ed6454a733bea09ae620517c4894701999e119))
|
||||
|
||||
## [2.30.2](https://github.com/payloadcms/payload/compare/v2.30.1...v2.30.2) (2024-10-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* applies resize after cropping if `resizeOptions` are defined ([#8535](https://github.com/payloadcms/payload/issues/8535)) ([f2284f3](https://github.com/payloadcms/payload/commit/f2284f3d1b420c543d9eee9929f961db4cbef8a1))
|
||||
* calculates correct aspect ratio dimensions on sharp based files ([#8510](https://github.com/payloadcms/payload/issues/8510)) ([9d05b82](https://github.com/payloadcms/payload/commit/9d05b82dc67b967e55176b92f7b20d4486883201)), closes [#8317](https://github.com/payloadcms/payload/issues/8317)
|
||||
* **db-postgres:** build indexes for relationships ([#8446](https://github.com/payloadcms/payload/issues/8446)) ([d05e3b0](https://github.com/payloadcms/payload/commit/d05e3b0411c2e705bd7a26e93ba55f2b7532e41b))
|
||||
* **db-postgres:** port many various fixes from 3.0 ([#8468](https://github.com/payloadcms/payload/issues/8468)) ([1347b6c](https://github.com/payloadcms/payload/commit/1347b6cc36c33043755b4e31d7731ba19b8c985f))
|
||||
* **db-postgres:** select hasMany nested to array + tab/group ([#8739](https://github.com/payloadcms/payload/issues/8739)) ([0efc610](https://github.com/payloadcms/payload/commit/0efc6102104729a162912157a7e34293ec9764a5))
|
||||
* **richtext-lexical:** add target _blank for new-tab in linkFeature ([#8571](https://github.com/payloadcms/payload/issues/8571)) ([61e8ce1](https://github.com/payloadcms/payload/commit/61e8ce17439301cb16aee92887e97a69cac4e044)), closes [#8569](https://github.com/payloadcms/payload/issues/8569)
|
||||
|
||||
## [2.30.1](https://github.com/payloadcms/payload/compare/v2.30.0...v2.30.1) (2024-10-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **db-mongodb:** properly filters out `number` field values with the `exists` operator filter ([#8415](https://github.com/payloadcms/payload/issues/8415)) ([0586f23](https://github.com/payloadcms/payload/commit/0586f236bbf04163a0d9b226772849cb3d977864)), closes [#8181](https://github.com/payloadcms/payload/issues/8181)
|
||||
* sorting by id incorrectly orders by version.id ([#8450](https://github.com/payloadcms/payload/issues/8450)) ([1d38e6d](https://github.com/payloadcms/payload/commit/1d38e6d5d5b56a91aa8f59a461d40f28b1750f8c))
|
||||
|
||||
## [2.30.0](https://github.com/payloadcms/payload/compare/v2.29.0...v2.30.0) (2024-09-27)
|
||||
|
||||
* export toast from react toastify in payload ([#8438](https://github.com/payloadcms/payload/issues/8438)) ([17fc2d1](https://github.com/payloadcms/payload/commit/17fc2d13d06b6de01f839c27fd706bc0d6a185eb))
|
||||
|
||||
## [2.29.0](https://github.com/payloadcms/payload/compare/v2.28.0...v2.29.0) (2024-09-25)
|
||||
|
||||
|
||||
|
||||
@@ -99,6 +99,10 @@ If you want to add contributions to this repository, please follow the instructi
|
||||
|
||||
The [Examples Directory](./examples) is a great resource for learning how to setup Payload in a variety of different ways, but you can also find great examples in our blog and throughout our social media.
|
||||
|
||||
If you'd like to run the examples, you can either copy them to a folder outside this repo or run them directly by (1) navigating to the example's subfolder (`cd examples/your-example-folder`) and (2) using the `--ignore-workspace` flag to bypass workspace restrictions (e.g., `pnpm --ignore-workspace install` or `pnpm --ignore-workspace dev`).
|
||||
|
||||
You can see more examples at:
|
||||
|
||||
- [Examples Directory](./examples)
|
||||
- [Payload Blog](https://payloadcms.com/blog)
|
||||
- [Payload YouTube](https://www.youtube.com/@payloadcms)
|
||||
|
||||
@@ -24,8 +24,8 @@ export default buildConfig({
|
||||
// collections go here
|
||||
],
|
||||
localization: {
|
||||
locales: ['en', 'es', 'de'],
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'es', 'de'], // required
|
||||
defaultLocale: 'en', // required
|
||||
fallback: true,
|
||||
},
|
||||
})
|
||||
@@ -54,7 +54,7 @@ export default buildConfig({
|
||||
rtl: true,
|
||||
},
|
||||
],
|
||||
defaultLocale: 'en',
|
||||
defaultLocale: 'en', // required
|
||||
fallback: true,
|
||||
},
|
||||
})
|
||||
@@ -87,7 +87,7 @@ export default buildConfig({
|
||||
code: 'nb',
|
||||
},
|
||||
],
|
||||
defaultLocale: 'en',
|
||||
defaultLocale: 'en', // required
|
||||
fallback: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -144,6 +144,10 @@ export default addLastModified
|
||||
|
||||
### Available Plugins
|
||||
|
||||
You can discover existing plugins by browsing the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin).
|
||||
Payload supports both official plugins, maintained by the Payload team, and community plugins, developed by external contributors.
|
||||
|
||||
You can discover existing plugins by browsing the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin). These plugins offer a wide range of functionality. Some are maintained by the Payload team, while others are community-built. While we encourage users to explore them, please note that only official plugins are maintained and supported by the Payload team. For community plugins, support may vary as they are developed and maintained independently.
|
||||
|
||||
For maintainers building plugins for others to use, please add the topic to help others find it. If you would like one to be built by the core Payload team, [open a Feature Request](https://github.com/payloadcms/payload/discussions) in our GitHub Discussions board. We would be happy to review your code and maybe feature you and your plugin where appropriate.
|
||||
|
||||
For a list of official plugins, check the [Payload monorepo](https://github.com/payloadcms/payload/tree/main/packages).
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"copyfiles": "2.4.1",
|
||||
"cross-env": "7.0.3",
|
||||
"dotenv": "8.6.0",
|
||||
"drizzle-orm": "0.29.3",
|
||||
"drizzle-orm": "0.32.1",
|
||||
"express": "4.18.2",
|
||||
"form-data": "3.0.1",
|
||||
"fs-extra": "10.1.0",
|
||||
|
||||
@@ -33,15 +33,15 @@
|
||||
"md5": "2.3.0",
|
||||
"mini-css-extract-plugin": "1.6.2",
|
||||
"path-browserify": "1.0.1",
|
||||
"postcss": "8.4.31",
|
||||
"postcss": "8.4.47",
|
||||
"postcss-loader": "6.2.1",
|
||||
"postcss-preset-env": "9.0.0",
|
||||
"process": "0.11.10",
|
||||
"sass-loader": "12.6.0",
|
||||
"style-loader": "^2.0.0",
|
||||
"swc-loader": "^0.2.3",
|
||||
"swc-minify-webpack-plugin": "^2.1.0",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"swc-loader": "^0.2.6",
|
||||
"swc-minify-webpack-plugin": "^2.1.3",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "^5.78.0",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "1.7.2",
|
||||
"version": "1.7.3",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -55,6 +55,30 @@ const handleNonHasManyValues = (formattedValue, operator, path) => {
|
||||
}
|
||||
}
|
||||
|
||||
const buildExistsQuery = (formattedValue, path) => {
|
||||
if (formattedValue) {
|
||||
return {
|
||||
rawQuery: {
|
||||
$and: [
|
||||
{ [path]: { $exists: true } },
|
||||
{ [path]: { $ne: null } },
|
||||
{ [path]: { $ne: '' } }, // Exclude null and empty string
|
||||
],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
rawQuery: {
|
||||
$or: [
|
||||
{ [path]: { $exists: false } },
|
||||
{ [path]: { $eq: null } },
|
||||
{ [path]: { $eq: '' } }, // Treat empty string as null / undefined
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const sanitizeQueryValue = ({
|
||||
field,
|
||||
hasCustomID,
|
||||
@@ -102,8 +126,16 @@ export const sanitizeQueryValue = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'number' && typeof formattedValue === 'string') {
|
||||
formattedValue = Number(val)
|
||||
if (field.type === 'number') {
|
||||
if (typeof formattedValue === 'string' && operator !== 'exists') {
|
||||
formattedValue = Number(val)
|
||||
}
|
||||
|
||||
if (operator === 'exists') {
|
||||
formattedValue = val === 'true' ? true : val === 'false' ? false : Boolean(val)
|
||||
|
||||
return buildExistsQuery(formattedValue, path)
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'date' && typeof val === 'string' && operator !== 'exists') {
|
||||
@@ -193,27 +225,7 @@ export const sanitizeQueryValue = ({
|
||||
if (operator === 'exists') {
|
||||
formattedValue = formattedValue === 'true' || formattedValue === true
|
||||
|
||||
if (formattedValue) {
|
||||
return {
|
||||
rawQuery: {
|
||||
$and: [
|
||||
{ [path]: { $exists: true } },
|
||||
{ [path]: { $ne: null } },
|
||||
{ [path]: { $ne: '' } },
|
||||
],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
rawQuery: {
|
||||
$or: [
|
||||
{ [path]: { $exists: false } },
|
||||
{ [path]: { $eq: null } },
|
||||
{ [path]: { $eq: '' } }, // Treat empty string as null / undefined
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
return buildExistsQuery(formattedValue, path)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "0.8.7",
|
||||
"version": "0.8.9",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,12 +26,12 @@
|
||||
"dependencies": {
|
||||
"@libsql/client": "^0.3.1",
|
||||
"console-table-printer": "2.11.2",
|
||||
"drizzle-kit": "0.20.14-1f2c838",
|
||||
"drizzle-orm": "0.29.3",
|
||||
"drizzle-kit": "0.23.2-df9e596",
|
||||
"drizzle-orm": "0.32.1",
|
||||
"pg": "8.11.3",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
"uuid": "9.0.0"
|
||||
"uuid": "10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { Payload } from 'payload'
|
||||
import type { Connect } from 'payload/database'
|
||||
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import { numeric, timestamp, varchar } from 'drizzle-orm/pg-core'
|
||||
import { Pool } from 'pg'
|
||||
import prompts from 'prompts'
|
||||
|
||||
import type { PostgresAdapter } from './types'
|
||||
|
||||
import { pushDevSchema } from './utilities/pushDevSchema'
|
||||
|
||||
const connectWithReconnect = async function ({
|
||||
adapter,
|
||||
payload,
|
||||
@@ -48,6 +48,7 @@ const connectWithReconnect = async function ({
|
||||
|
||||
export const connect: Connect = async function connect(this: PostgresAdapter, payload) {
|
||||
this.schema = {
|
||||
pgSchema: this.pgSchema,
|
||||
...this.tables,
|
||||
...this.relations,
|
||||
...this.enums,
|
||||
@@ -77,76 +78,10 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
|
||||
|
||||
// Only push schema if not in production
|
||||
if (
|
||||
process.env.NODE_ENV === 'production' ||
|
||||
process.env.PAYLOAD_MIGRATING === 'true' ||
|
||||
this.push === false
|
||||
)
|
||||
return
|
||||
|
||||
const { pushSchema } = require('drizzle-kit/payload')
|
||||
|
||||
// This will prompt if clarifications are needed for Drizzle to push new schema
|
||||
const { apply, hasDataLoss, statementsToExecute, warnings } = await pushSchema(
|
||||
this.schema,
|
||||
this.drizzle,
|
||||
)
|
||||
|
||||
if (warnings.length) {
|
||||
let message = `Warnings detected during schema push: \n\n${warnings.join('\n')}\n\n`
|
||||
|
||||
if (hasDataLoss) {
|
||||
message += `DATA LOSS WARNING: Possible data loss detected if schema is pushed.\n\n`
|
||||
}
|
||||
|
||||
message += `Accept warnings and push schema to database?`
|
||||
|
||||
const { confirm: acceptWarnings } = await prompts(
|
||||
{
|
||||
name: 'confirm',
|
||||
type: 'confirm',
|
||||
initial: false,
|
||||
message,
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Exit if user does not accept warnings.
|
||||
// Q: Is this the right type of exit for this interaction?
|
||||
if (!acceptWarnings) {
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
await apply()
|
||||
|
||||
// Migration table def in order to use query using drizzle
|
||||
const migrationsSchema = this.pgSchema.table('payload_migrations', {
|
||||
name: varchar('name'),
|
||||
batch: numeric('batch'),
|
||||
created_at: timestamp('created_at'),
|
||||
updated_at: timestamp('updated_at'),
|
||||
})
|
||||
|
||||
const devPush = await this.drizzle
|
||||
.select()
|
||||
.from(migrationsSchema)
|
||||
.where(eq(migrationsSchema.batch, '-1'))
|
||||
|
||||
if (!devPush.length) {
|
||||
await this.drizzle.insert(migrationsSchema).values({
|
||||
name: 'dev',
|
||||
batch: '-1',
|
||||
})
|
||||
} else {
|
||||
await this.drizzle
|
||||
.update(migrationsSchema)
|
||||
.set({
|
||||
updated_at: new Date(),
|
||||
})
|
||||
.where(eq(migrationsSchema.batch, '-1'))
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
process.env.PAYLOAD_MIGRATING !== 'true' &&
|
||||
this.push !== false
|
||||
) {
|
||||
await pushDevSchema(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Count } from 'payload/database'
|
||||
import type { SanitizedCollectionConfig } from 'payload/types'
|
||||
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { sql, count as sqlCount } from 'drizzle-orm'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { ChainedMethods } from './find/chainMethods'
|
||||
@@ -51,8 +51,11 @@ export const count: Count = async function count(
|
||||
methods: selectCountMethods,
|
||||
query: db
|
||||
.select({
|
||||
count: sql<number>`count
|
||||
(DISTINCT ${this.tables[tableName].id})`,
|
||||
count:
|
||||
selectCountMethods.length > 0
|
||||
? sql<number>`count
|
||||
(DISTINCT ${this.tables[tableName].id})`
|
||||
: sqlCount(),
|
||||
})
|
||||
.from(table)
|
||||
.where(where),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-restricted-syntax, no-await-in-loop */
|
||||
import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
|
||||
import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
|
||||
import type { CreateMigration } from 'payload/database'
|
||||
|
||||
import fs from 'fs'
|
||||
@@ -43,12 +43,13 @@ const getDefaultDrizzleSnapshot = (): DrizzleSnapshotJSON => ({
|
||||
schemas: {},
|
||||
tables: {},
|
||||
},
|
||||
dialect: 'pg',
|
||||
dialect: 'postgresql',
|
||||
enums: {},
|
||||
prevId: '00000000-0000-0000-0000-00000000000',
|
||||
schemas: {},
|
||||
sequences: {},
|
||||
tables: {},
|
||||
version: '5',
|
||||
version: '7',
|
||||
})
|
||||
|
||||
export const createMigration: CreateMigration = async function createMigration(
|
||||
@@ -60,7 +61,7 @@ export const createMigration: CreateMigration = async function createMigration(
|
||||
fs.mkdirSync(dir)
|
||||
}
|
||||
|
||||
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/payload')
|
||||
const { generateDrizzleJson, generateMigration, upPgSnapshot } = require('drizzle-kit/api')
|
||||
|
||||
const [yyymmdd, hhmmss] = new Date().toISOString().split('T')
|
||||
const formattedDate = yyymmdd.replace(/\D/g, '')
|
||||
@@ -76,6 +77,12 @@ export const createMigration: CreateMigration = async function createMigration(
|
||||
|
||||
let drizzleJsonBefore = getDefaultDrizzleSnapshot()
|
||||
|
||||
if (this.schemaName) {
|
||||
drizzleJsonBefore.schemas = {
|
||||
[this.schemaName]: this.schemaName,
|
||||
}
|
||||
}
|
||||
|
||||
// Get latest migration snapshot
|
||||
const latestSnapshot = fs
|
||||
.readdirSync(dir)
|
||||
@@ -92,6 +99,11 @@ export const createMigration: CreateMigration = async function createMigration(
|
||||
}
|
||||
|
||||
const drizzleJsonAfter = generateDrizzleJson(this.schema)
|
||||
|
||||
if (drizzleJsonBefore.version < drizzleJsonAfter.version) {
|
||||
drizzleJsonBefore = upPgSnapshot(drizzleJsonBefore)
|
||||
}
|
||||
|
||||
const sqlStatementsUp = await generateMigration(drizzleJsonBefore, drizzleJsonAfter)
|
||||
const sqlStatementsDown = await generateMigration(drizzleJsonAfter, drizzleJsonBefore)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FindArgs } from 'payload/database'
|
||||
import type { Field, PayloadRequest, TypeWithID } from 'payload/types'
|
||||
|
||||
import { inArray, sql } from 'drizzle-orm'
|
||||
import { inArray, sql, count as sqlCount } from 'drizzle-orm'
|
||||
|
||||
import type { PostgresAdapter } from '../types'
|
||||
import type { ChainedMethods } from './chainMethods'
|
||||
@@ -143,8 +143,11 @@ export const findMany = async function find({
|
||||
methods: selectCountMethods,
|
||||
query: db
|
||||
.select({
|
||||
count: sql<number>`count
|
||||
(DISTINCT ${adapter.tables[tableName].id})`,
|
||||
count:
|
||||
selectCountMethods.length > 0
|
||||
? sql<number>`count
|
||||
(DISTINCT ${adapter.tables[tableName].id})`
|
||||
: sqlCount(),
|
||||
})
|
||||
.from(table)
|
||||
.where(where),
|
||||
|
||||
@@ -14,11 +14,11 @@ export const init: Init = async function init(this: PostgresAdapter) {
|
||||
if (this.schemaName) {
|
||||
this.pgSchema = pgSchema(this.schemaName)
|
||||
} else {
|
||||
this.pgSchema = { table: pgTable }
|
||||
this.pgSchema = { enum: pgEnum, table: pgTable }
|
||||
}
|
||||
|
||||
if (this.payload.config.localization) {
|
||||
this.enums.enum__locales = pgEnum(
|
||||
this.enums.enum__locales = this.pgSchema.enum(
|
||||
'_locales',
|
||||
this.payload.config.localization.locales.map(({ code }) => code) as [string, ...string[]],
|
||||
)
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
|
||||
let latestBatch = 0
|
||||
let migrationsInDB = []
|
||||
|
||||
const hasMigrationTable = await migrationTableExists(this.drizzle)
|
||||
const hasMigrationTable = await migrationTableExists(this)
|
||||
|
||||
if (hasMigrationTable) {
|
||||
;({ docs: migrationsInDB } = await payload.find({
|
||||
@@ -80,7 +80,7 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
|
||||
}
|
||||
|
||||
async function runMigrationFile(payload: Payload, migration: Migration, batch: number) {
|
||||
const { generateDrizzleJson } = require('drizzle-kit/payload')
|
||||
const { generateDrizzleJson } = require('drizzle-kit/api')
|
||||
|
||||
const start = Date.now()
|
||||
const req = { payload } as PayloadRequest
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function migrateDown(this: PostgresAdapter): Promise<void> {
|
||||
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
|
||||
})
|
||||
|
||||
const tableExists = await migrationTableExists(this.drizzle)
|
||||
const tableExists = await migrationTableExists(this)
|
||||
if (tableExists) {
|
||||
await payload.delete({
|
||||
id: migration.id,
|
||||
|
||||
@@ -51,7 +51,7 @@ export async function migrateRefresh(this: PostgresAdapter) {
|
||||
msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`,
|
||||
})
|
||||
|
||||
const tableExists = await migrationTableExists(this.drizzle)
|
||||
const tableExists = await migrationTableExists(this)
|
||||
if (tableExists) {
|
||||
await payload.delete({
|
||||
collection: 'payload-migrations',
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function migrateReset(this: PostgresAdapter): Promise<void> {
|
||||
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
|
||||
})
|
||||
|
||||
const tableExists = await migrationTableExists(this.drizzle)
|
||||
const tableExists = await migrationTableExists(this)
|
||||
if (tableExists) {
|
||||
await payload.delete({
|
||||
id: migration.id,
|
||||
@@ -68,7 +68,7 @@ export async function migrateReset(this: PostgresAdapter): Promise<void> {
|
||||
|
||||
// Delete dev migration
|
||||
|
||||
const tableExists = await migrationTableExists(this.drizzle)
|
||||
const tableExists = await migrationTableExists(this)
|
||||
if (tableExists) {
|
||||
try {
|
||||
await payload.delete({
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function migrateStatus(this: PostgresAdapter): Promise<void> {
|
||||
})
|
||||
|
||||
let existingMigrations = []
|
||||
const hasMigrationTable = await migrationTableExists(this.drizzle)
|
||||
const hasMigrationTable = await migrationTableExists(this)
|
||||
|
||||
if (hasMigrationTable) {
|
||||
;({ existingMigrations } = await getMigrations({ payload }))
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { SQL } from 'drizzle-orm'
|
||||
import type { Field, Operator, Where } from 'payload/types'
|
||||
|
||||
import { and, ilike, isNotNull, isNull, ne, notInArray, or, sql } from 'drizzle-orm'
|
||||
import { PgUUID } from 'drizzle-orm/pg-core'
|
||||
import { QueryError } from 'payload/errors'
|
||||
import { validOperators } from 'payload/types'
|
||||
|
||||
@@ -174,6 +175,7 @@ export async function parseParams({
|
||||
const sanitizedQueryValue = sanitizeQueryValue({
|
||||
adapter,
|
||||
field,
|
||||
isUUID: table?.[columnName] instanceof PgUUID,
|
||||
operator,
|
||||
relationOrPath,
|
||||
val,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { APIError } from 'payload/errors'
|
||||
import { type Field, type TabAsField, fieldAffectsData } from 'payload/types'
|
||||
import { createArrayFromCommaDelineated } from 'payload/utilities'
|
||||
import { validate as uuidValidate } from 'uuid'
|
||||
|
||||
import type { PostgresAdapter } from '../types'
|
||||
|
||||
type SanitizeQueryValueArgs = {
|
||||
adapter: PostgresAdapter
|
||||
field: Field | TabAsField
|
||||
isUUID: boolean
|
||||
operator: string
|
||||
relationOrPath: string
|
||||
val: any
|
||||
@@ -15,6 +17,7 @@ type SanitizeQueryValueArgs = {
|
||||
export const sanitizeQueryValue = ({
|
||||
adapter,
|
||||
field,
|
||||
isUUID,
|
||||
operator: operatorArg,
|
||||
relationOrPath,
|
||||
val,
|
||||
@@ -64,6 +67,16 @@ export const sanitizeQueryValue = ({
|
||||
|
||||
if (field.type === 'number' && typeof formattedValue === 'string') {
|
||||
formattedValue = Number(val)
|
||||
|
||||
if (Number.isNaN(formattedValue)) {
|
||||
formattedValue = null
|
||||
}
|
||||
}
|
||||
|
||||
if (isUUID && typeof formattedValue === 'string') {
|
||||
if (!uuidValidate(val)) {
|
||||
formattedValue = null
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'date' && operator !== 'exists') {
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
PgTableWithColumns,
|
||||
UniqueConstraintBuilder,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import { Field, fieldAffectsData } from 'payload/types'
|
||||
import type { Field } from 'payload/types'
|
||||
|
||||
import { relations } from 'drizzle-orm'
|
||||
import {
|
||||
@@ -20,10 +20,12 @@ import {
|
||||
unique,
|
||||
varchar,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import { fieldAffectsData } from 'payload/types'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { GenericColumns, GenericTable, IDType, PostgresAdapter } from '../types'
|
||||
|
||||
import { createIndex } from './createIndex'
|
||||
import { createTableName } from './createTableName'
|
||||
import { parentIDColumnMap } from './parentIDColumnMap'
|
||||
import { setColumnID } from './setColumnID'
|
||||
@@ -51,9 +53,17 @@ type Args = {
|
||||
tableName: string
|
||||
timestamps?: boolean
|
||||
versions: boolean
|
||||
/**
|
||||
* Tracks whether or not this table is built
|
||||
* from the result of a localized array or block field at some point
|
||||
*/
|
||||
withinLocalizedArrayOrBlock?: boolean
|
||||
}
|
||||
|
||||
type Result = {
|
||||
hasLocalizedManyNumberField: boolean
|
||||
hasLocalizedManyTextField: boolean
|
||||
hasLocalizedRelationshipField: boolean
|
||||
hasManyNumberField: 'index' | boolean
|
||||
hasManyTextField: 'index' | boolean
|
||||
relationsToBuild: Map<string, string>
|
||||
@@ -76,6 +86,7 @@ export const buildTable = ({
|
||||
tableName,
|
||||
timestamps,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
}: Args): Result => {
|
||||
const rootTableName = incomingRootTableName || tableName
|
||||
const columns: Record<string, PgColumnBuilder> = baseColumns
|
||||
@@ -124,6 +135,7 @@ export const buildTable = ({
|
||||
rootTableIDColType: rootTableIDColType || idColType,
|
||||
rootTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
|
||||
if (timestamps) {
|
||||
@@ -328,16 +340,28 @@ export const buildTable = ({
|
||||
if (relatedCollectionCustomIDType === 'number') colType = 'numeric'
|
||||
if (relatedCollectionCustomIDType === 'text') colType = 'varchar'
|
||||
|
||||
relationshipColumns[`${relationTo}ID`] = parentIDColumnMap[colType](
|
||||
`${formattedRelationTo}_id`,
|
||||
)
|
||||
const colName = `${relationTo}ID`
|
||||
|
||||
relationshipColumns[colName] = parentIDColumnMap[colType](`${formattedRelationTo}_id`)
|
||||
|
||||
relationExtraConfig[`${relationTo}IdFk`] = (cols) =>
|
||||
foreignKey({
|
||||
name: `${relationshipsTableName}_${toSnakeCase(relationTo)}_fk`,
|
||||
columns: [cols[`${relationTo}ID`]],
|
||||
columns: [cols[colName]],
|
||||
foreignColumns: [adapter.tables[formattedRelationTo].id],
|
||||
}).onDelete('cascade')
|
||||
|
||||
const indexName = [colName]
|
||||
|
||||
if (hasLocalizedRelationshipField) {
|
||||
indexName.push('locale')
|
||||
}
|
||||
|
||||
relationExtraConfig[`${relationTo}IdIdx`] = createIndex({
|
||||
name: indexName,
|
||||
columnName: `${formattedRelationTo}_id`,
|
||||
tableName: relationshipsTableName,
|
||||
})
|
||||
})
|
||||
|
||||
relationshipsTable = adapter.pgSchema.table(
|
||||
@@ -431,5 +455,12 @@ export const buildTable = ({
|
||||
|
||||
adapter.relations[`relations_${tableName}`] = tableRelations
|
||||
|
||||
return { hasManyNumberField, hasManyTextField, relationsToBuild }
|
||||
return {
|
||||
hasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField,
|
||||
hasManyNumberField,
|
||||
hasManyTextField,
|
||||
relationsToBuild,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
integer,
|
||||
jsonb,
|
||||
numeric,
|
||||
pgEnum,
|
||||
text,
|
||||
timestamp,
|
||||
varchar,
|
||||
@@ -57,6 +56,11 @@ type Args = {
|
||||
rootTableIDColType: string
|
||||
rootTableName: string
|
||||
versions: boolean
|
||||
/**
|
||||
* Tracks whether or not this table is built
|
||||
* from the result of a localized array or block field at some point
|
||||
*/
|
||||
withinLocalizedArrayOrBlock?: boolean
|
||||
}
|
||||
|
||||
type Result = {
|
||||
@@ -91,6 +95,7 @@ export const traverseFields = ({
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
}: Args): Result => {
|
||||
const throwValidationError = true
|
||||
let hasLocalizedField = false
|
||||
@@ -152,7 +157,11 @@ export const traverseFields = ({
|
||||
switch (field.type) {
|
||||
case 'text': {
|
||||
if (field.hasMany) {
|
||||
if (field.localized) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock
|
||||
|
||||
if (isLocalized) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
|
||||
@@ -181,7 +190,12 @@ export const traverseFields = ({
|
||||
|
||||
case 'number': {
|
||||
if (field.hasMany) {
|
||||
if (field.localized) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
hasLocalizedManyNumberField = true
|
||||
}
|
||||
|
||||
@@ -232,7 +246,7 @@ export const traverseFields = ({
|
||||
throwValidationError,
|
||||
})
|
||||
|
||||
adapter.enums[enumName] = pgEnum(
|
||||
adapter.enums[enumName] = adapter.pgSchema.enum(
|
||||
enumName,
|
||||
field.options.map((option) => {
|
||||
if (optionIsObject(option)) {
|
||||
@@ -268,7 +282,12 @@ export const traverseFields = ({
|
||||
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
|
||||
}
|
||||
|
||||
if (field.localized) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
baseColumns.locale = adapter.enums.enum__locales('locale').notNull()
|
||||
baseExtraConfig.localeIdx = (cols) =>
|
||||
index(`${selectTableName}_locale_idx`).on(cols.locale)
|
||||
@@ -342,13 +361,21 @@ export const traverseFields = ({
|
||||
_parentIDIdx: (cols) => index(`${arrayTableName}_parent_id_idx`).on(cols._parentID),
|
||||
}
|
||||
|
||||
if (field.localized && adapter.payload.config.localization) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
||||
baseExtraConfig._localeIdx = (cols) =>
|
||||
index(`${arrayTableName}_locale_idx`).on(cols._locale)
|
||||
}
|
||||
|
||||
const {
|
||||
hasLocalizedManyNumberField: subHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: subHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: subHasLocalizedRelationshipField,
|
||||
hasManyNumberField: subHasManyNumberField,
|
||||
hasManyTextField: subHasManyTextField,
|
||||
relationsToBuild: subRelationsToBuild,
|
||||
@@ -365,8 +392,21 @@ export const traverseFields = ({
|
||||
rootTableName,
|
||||
tableName: arrayTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock: isLocalized,
|
||||
})
|
||||
|
||||
if (subHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = subHasLocalizedManyNumberField
|
||||
}
|
||||
|
||||
if (subHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = subHasLocalizedRelationshipField
|
||||
}
|
||||
|
||||
if (subHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = subHasLocalizedManyTextField
|
||||
}
|
||||
|
||||
if (subHasManyTextField) {
|
||||
if (!hasManyTextField || subHasManyTextField === 'index')
|
||||
hasManyTextField = subHasManyTextField
|
||||
@@ -433,13 +473,21 @@ export const traverseFields = ({
|
||||
_pathIdx: (cols) => index(`${blockTableName}_path_idx`).on(cols._path),
|
||||
}
|
||||
|
||||
if (field.localized && adapter.payload.config.localization) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
||||
baseExtraConfig._localeIdx = (cols) =>
|
||||
index(`${blockTableName}_locale_idx`).on(cols._locale)
|
||||
}
|
||||
|
||||
const {
|
||||
hasLocalizedManyNumberField: subHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: subHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: subHasLocalizedRelationshipField,
|
||||
hasManyNumberField: subHasManyNumberField,
|
||||
hasManyTextField: subHasManyTextField,
|
||||
relationsToBuild: subRelationsToBuild,
|
||||
@@ -456,8 +504,21 @@ export const traverseFields = ({
|
||||
rootTableName,
|
||||
tableName: blockTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock: isLocalized,
|
||||
})
|
||||
|
||||
if (subHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = subHasLocalizedManyNumberField
|
||||
}
|
||||
|
||||
if (subHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = subHasLocalizedRelationshipField
|
||||
}
|
||||
|
||||
if (subHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = subHasLocalizedManyTextField
|
||||
}
|
||||
|
||||
if (subHasManyTextField) {
|
||||
if (!hasManyTextField || subHasManyTextField === 'index')
|
||||
hasManyTextField = subHasManyTextField
|
||||
@@ -541,6 +602,7 @@ export const traverseFields = ({
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
|
||||
if (groupHasLocalizedField) hasLocalizedField = true
|
||||
@@ -584,6 +646,7 @@ export const traverseFields = ({
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized,
|
||||
})
|
||||
|
||||
if (groupHasLocalizedField) hasLocalizedField = true
|
||||
@@ -628,6 +691,7 @@ export const traverseFields = ({
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
|
||||
if (tabHasLocalizedField) hasLocalizedField = true
|
||||
@@ -672,6 +736,7 @@ export const traverseFields = ({
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
|
||||
if (rowHasLocalizedField) hasLocalizedField = true
|
||||
@@ -691,7 +756,10 @@ export const traverseFields = ({
|
||||
relationships.add(field.relationTo)
|
||||
}
|
||||
|
||||
if (field.localized && adapter.payload.config.localization) {
|
||||
if (
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock
|
||||
) {
|
||||
hasLocalizedRelationshipField = true
|
||||
}
|
||||
break
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import type { NumberField } from 'payload/types'
|
||||
|
||||
type Args = {
|
||||
@@ -6,10 +5,29 @@ type Args = {
|
||||
locale?: string
|
||||
numberRows: Record<string, unknown>[]
|
||||
ref: Record<string, unknown>
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
|
||||
export const transformHasManyNumber = ({ field, locale, numberRows, ref }: Args) => {
|
||||
const result = numberRows.map(({ number }) => number)
|
||||
export const transformHasManyNumber = ({
|
||||
field,
|
||||
locale,
|
||||
numberRows,
|
||||
ref,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
let result: unknown[]
|
||||
|
||||
if (withinArrayOrBlockLocale) {
|
||||
result = numberRows.reduce((acc, { locale, number }) => {
|
||||
if (locale === withinArrayOrBlockLocale) {
|
||||
acc.push(number)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
} else {
|
||||
result = numberRows.map(({ number }) => number)
|
||||
}
|
||||
|
||||
if (locale) {
|
||||
ref[field.name][locale] = result
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import type { TextField } from 'payload/types'
|
||||
|
||||
type Args = {
|
||||
@@ -6,10 +5,29 @@ type Args = {
|
||||
locale?: string
|
||||
ref: Record<string, unknown>
|
||||
textRows: Record<string, unknown>[]
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
|
||||
export const transformHasManyText = ({ field, locale, ref, textRows }: Args) => {
|
||||
const result = textRows.map(({ text }) => text)
|
||||
export const transformHasManyText = ({
|
||||
field,
|
||||
locale,
|
||||
ref,
|
||||
textRows,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
let result: unknown[]
|
||||
|
||||
if (withinArrayOrBlockLocale) {
|
||||
result = textRows.reduce((acc, { locale, text }) => {
|
||||
if (locale === withinArrayOrBlockLocale) {
|
||||
acc.push(text)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
} else {
|
||||
result = textRows.map(({ text }) => text)
|
||||
}
|
||||
|
||||
if (locale) {
|
||||
ref[field.name][locale] = result
|
||||
|
||||
@@ -6,21 +6,31 @@ type Args = {
|
||||
locale?: string
|
||||
ref: Record<string, unknown>
|
||||
relations: Record<string, unknown>[]
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
|
||||
export const transformRelationship = ({ field, locale, ref, relations }: Args) => {
|
||||
export const transformRelationship = ({
|
||||
field,
|
||||
locale,
|
||||
ref,
|
||||
relations,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
let result: unknown
|
||||
|
||||
if (!('hasMany' in field) || field.hasMany === false) {
|
||||
const relation = relations[0]
|
||||
let relation = relations[0]
|
||||
|
||||
if (withinArrayOrBlockLocale) {
|
||||
relation = relations.find((rel) => rel.locale === withinArrayOrBlockLocale)
|
||||
}
|
||||
|
||||
if (relation) {
|
||||
// Handle hasOne Poly
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
const matchedRelation = Object.entries(relation).find(
|
||||
([key, val]) =>
|
||||
val !== null && !['id', 'locale', 'order', 'parent', 'path'].includes(key),
|
||||
)
|
||||
const matchedRelation = Object.entries(relation).find(([key, val]) => {
|
||||
return val !== null && !['id', 'locale', 'order', 'parent', 'path'].includes(key)
|
||||
})
|
||||
|
||||
if (matchedRelation) {
|
||||
const relationTo = matchedRelation[0].replace('ID', '')
|
||||
@@ -40,18 +50,26 @@ export const transformRelationship = ({ field, locale, ref, relations }: Args) =
|
||||
const transformedRelations = []
|
||||
|
||||
relations.forEach((relation) => {
|
||||
let matchedLocale = true
|
||||
|
||||
if (withinArrayOrBlockLocale) {
|
||||
matchedLocale = relation.locale === withinArrayOrBlockLocale
|
||||
}
|
||||
|
||||
// Handle hasMany
|
||||
if (!Array.isArray(field.relationTo)) {
|
||||
const relatedData = relation[`${field.relationTo}ID`]
|
||||
|
||||
if (relatedData) {
|
||||
if (relatedData && matchedLocale) {
|
||||
transformedRelations.push(relatedData)
|
||||
}
|
||||
} else {
|
||||
// Handle hasMany Poly
|
||||
const matchedRelation = Object.entries(relation).find(
|
||||
([key, val]) =>
|
||||
val !== null && !['id', 'locale', 'order', 'parent', 'path'].includes(key),
|
||||
val !== null &&
|
||||
!['id', 'locale', 'order', 'parent', 'path'].includes(key) &&
|
||||
matchedLocale,
|
||||
)
|
||||
|
||||
if (matchedRelation) {
|
||||
|
||||
@@ -55,6 +55,10 @@ type TraverseFieldsArgs = {
|
||||
* All hasMany text fields, as returned by Drizzle, keyed on an object by field path
|
||||
*/
|
||||
texts: Record<string, Record<string, unknown>[]>
|
||||
/**
|
||||
* Set to a locale if this group of fields is within a localized array or block.
|
||||
*/
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
|
||||
// Traverse fields recursively, transforming data
|
||||
@@ -71,6 +75,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
relationships,
|
||||
table,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
}: TraverseFieldsArgs): T => {
|
||||
const sanitizedPath = path ? `${path}.` : path
|
||||
|
||||
@@ -88,6 +93,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
relationships,
|
||||
table,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -108,6 +114,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
relationships,
|
||||
table,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -145,6 +152,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
relationships,
|
||||
table: row,
|
||||
texts,
|
||||
withinArrayOrBlockLocale: locale,
|
||||
})
|
||||
|
||||
if ('_order' in rowResult) {
|
||||
@@ -157,7 +165,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
return arrayResult
|
||||
}, {})
|
||||
} else {
|
||||
result[field.name] = fieldData.map((row, i) => {
|
||||
result[field.name] = fieldData.reduce((acc, row, i) => {
|
||||
if (row._uuid) {
|
||||
row.id = row._uuid
|
||||
delete row._uuid
|
||||
@@ -167,34 +175,48 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
delete row._order
|
||||
}
|
||||
|
||||
return traverseFields<T>({
|
||||
blocks,
|
||||
config,
|
||||
dataRef: row,
|
||||
deletions,
|
||||
fieldPrefix: '',
|
||||
fields: field.fields,
|
||||
numbers,
|
||||
path: `${sanitizedPath}${field.name}.${i}`,
|
||||
relationships,
|
||||
table: row,
|
||||
texts,
|
||||
})
|
||||
})
|
||||
if (
|
||||
!withinArrayOrBlockLocale ||
|
||||
(withinArrayOrBlockLocale && withinArrayOrBlockLocale === row._locale)
|
||||
) {
|
||||
if (row._locale) {
|
||||
delete row._locale
|
||||
}
|
||||
|
||||
acc.push(
|
||||
traverseFields<T>({
|
||||
blocks,
|
||||
config,
|
||||
dataRef: row,
|
||||
deletions,
|
||||
fieldPrefix: '',
|
||||
fields: field.fields,
|
||||
numbers,
|
||||
path: `${sanitizedPath}${field.name}.${i}`,
|
||||
relationships,
|
||||
table: row,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
if (field.type === 'blocks') {
|
||||
const blockFieldPath = `${sanitizedPath}${field.name}`
|
||||
const blocksByPath = blocks[blockFieldPath]
|
||||
|
||||
if (Array.isArray(blocks[blockFieldPath])) {
|
||||
if (Array.isArray(blocksByPath)) {
|
||||
if (field.localized) {
|
||||
result[field.name] = {}
|
||||
|
||||
blocks[blockFieldPath].forEach((row) => {
|
||||
blocksByPath.forEach((row) => {
|
||||
if (row._uuid) {
|
||||
row.id = row._uuid
|
||||
delete row._uuid
|
||||
@@ -223,6 +245,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
relationships,
|
||||
table: row,
|
||||
texts,
|
||||
withinArrayOrBlockLocale: locale,
|
||||
})
|
||||
|
||||
delete blockResult._order
|
||||
@@ -233,7 +256,23 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
})
|
||||
})
|
||||
} else {
|
||||
result[field.name] = blocks[blockFieldPath].map((row, i) => {
|
||||
// Add locale-specific index to have a proper blockFieldPath for current locale
|
||||
// because blocks can be in the same array for different locales!
|
||||
if (withinArrayOrBlockLocale && config.localization) {
|
||||
for (const locale of config.localization.localeCodes) {
|
||||
let localeIndex = 0
|
||||
|
||||
for (let i = 0; i < blocksByPath.length; i++) {
|
||||
const row = blocksByPath[i]
|
||||
if (row._locale === locale) {
|
||||
row._index = localeIndex
|
||||
localeIndex++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result[field.name] = blocksByPath.reduce((acc, row, i) => {
|
||||
delete row._order
|
||||
if (row._uuid) {
|
||||
row.id = row._uuid
|
||||
@@ -242,23 +281,43 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
const block = field.blocks.find(({ slug }) => slug === row.blockType)
|
||||
|
||||
if (block) {
|
||||
return traverseFields<T>({
|
||||
blocks,
|
||||
config,
|
||||
dataRef: row,
|
||||
deletions,
|
||||
fieldPrefix: '',
|
||||
fields: block.fields,
|
||||
numbers,
|
||||
path: `${blockFieldPath}.${i}`,
|
||||
relationships,
|
||||
table: row,
|
||||
texts,
|
||||
})
|
||||
if (
|
||||
!withinArrayOrBlockLocale ||
|
||||
(withinArrayOrBlockLocale && withinArrayOrBlockLocale === row._locale)
|
||||
) {
|
||||
if (row._locale) {
|
||||
delete row._locale
|
||||
}
|
||||
if (typeof row._index === 'number') {
|
||||
i = row._index
|
||||
delete row._index
|
||||
}
|
||||
|
||||
acc.push(
|
||||
traverseFields<T>({
|
||||
blocks,
|
||||
config,
|
||||
dataRef: row,
|
||||
deletions,
|
||||
fieldPrefix: '',
|
||||
fields: block.fields,
|
||||
numbers,
|
||||
path: `${blockFieldPath}.${i}`,
|
||||
relationships,
|
||||
table: row,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
}),
|
||||
)
|
||||
|
||||
return acc
|
||||
}
|
||||
} else {
|
||||
acc.push({})
|
||||
}
|
||||
|
||||
return {}
|
||||
})
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +364,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
field,
|
||||
ref: result,
|
||||
relations: relationPathMatch,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -339,6 +399,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
field,
|
||||
ref: result,
|
||||
textRows: textPathMatch,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -373,6 +434,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
field,
|
||||
numberRows: numberPathMatch,
|
||||
ref: result,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -391,7 +453,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
return selectResult
|
||||
}, {})
|
||||
} else {
|
||||
result[field.name] = fieldData.map(({ value }) => value)
|
||||
let selectData = fieldData
|
||||
if (withinArrayOrBlockLocale) {
|
||||
selectData = selectData.filter(({ locale }) => locale === withinArrayOrBlockLocale)
|
||||
}
|
||||
result[field.name] = selectData.map(({ value }) => value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -404,8 +470,20 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
}[] = []
|
||||
|
||||
if (field.localized && Array.isArray(table._locales)) {
|
||||
if (!table._locales.length && config.localization) {
|
||||
config.localization.localeCodes.forEach((_locale) =>
|
||||
(table._locales as unknown[]).push({ _locale }),
|
||||
)
|
||||
}
|
||||
|
||||
table._locales.forEach((localeRow) => {
|
||||
valuesToTransform.push({ ref: localizedFieldData, table: localeRow })
|
||||
valuesToTransform.push({
|
||||
ref: localizedFieldData,
|
||||
table: {
|
||||
...table,
|
||||
...localeRow,
|
||||
},
|
||||
})
|
||||
})
|
||||
} else {
|
||||
valuesToTransform.push({ ref: result, table })
|
||||
@@ -419,50 +497,28 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
case 'tab':
|
||||
case 'group': {
|
||||
const groupFieldPrefix = `${fieldPrefix || ''}${field.name}_`
|
||||
const groupData = {}
|
||||
const locale = table._locale as string
|
||||
const refKey = field.localized && locale ? locale : field.name
|
||||
|
||||
if (field.localized) {
|
||||
if (typeof locale === 'string' && !ref[locale]) {
|
||||
ref[locale] = {}
|
||||
delete table._locale
|
||||
}
|
||||
if (field.localized && locale) delete table._locale
|
||||
ref[refKey] = traverseFields<Record<string, unknown>>({
|
||||
blocks,
|
||||
config,
|
||||
dataRef: groupData as Record<string, unknown>,
|
||||
deletions,
|
||||
fieldPrefix: groupFieldPrefix,
|
||||
fields: field.fields,
|
||||
numbers,
|
||||
path: `${sanitizedPath}${field.name}`,
|
||||
relationships,
|
||||
table,
|
||||
texts,
|
||||
withinArrayOrBlockLocale: locale || withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
Object.entries(ref).forEach(([groupLocale, groupLocaleData]) => {
|
||||
ref[groupLocale] = traverseFields<Record<string, unknown>>({
|
||||
blocks,
|
||||
config,
|
||||
dataRef: groupLocaleData as Record<string, unknown>,
|
||||
deletions,
|
||||
fieldPrefix: groupFieldPrefix,
|
||||
fields: field.fields,
|
||||
numbers,
|
||||
path: `${sanitizedPath}${field.name}`,
|
||||
relationships,
|
||||
table,
|
||||
texts,
|
||||
})
|
||||
})
|
||||
if ('_order' in ref) {
|
||||
delete ref._order
|
||||
}
|
||||
} else {
|
||||
const groupData = {}
|
||||
|
||||
ref[field.name] = traverseFields<Record<string, unknown>>({
|
||||
blocks,
|
||||
config,
|
||||
dataRef: groupData as Record<string, unknown>,
|
||||
deletions,
|
||||
fieldPrefix: groupFieldPrefix,
|
||||
fields: field.fields,
|
||||
numbers,
|
||||
path: `${sanitizedPath}${field.name}`,
|
||||
relationships,
|
||||
table,
|
||||
texts,
|
||||
})
|
||||
if ('_order' in ref) {
|
||||
delete ref._order
|
||||
}
|
||||
if ('_order' in ref) {
|
||||
delete ref._order
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
@@ -26,6 +26,11 @@ type Args = {
|
||||
[tableName: string]: Record<string, unknown>[]
|
||||
}
|
||||
texts: Record<string, unknown>[]
|
||||
/**
|
||||
* Set to a locale code if this set of fields is traversed within a
|
||||
* localized array or block field
|
||||
*/
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
|
||||
export const transformArray = ({
|
||||
@@ -43,6 +48,7 @@ export const transformArray = ({
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
const newRows: ArrayRowToInsert[] = []
|
||||
|
||||
@@ -78,6 +84,10 @@ export const transformArray = ({
|
||||
newRow.row._locale = locale
|
||||
}
|
||||
|
||||
if (withinArrayOrBlockLocale) {
|
||||
newRow.row._locale = withinArrayOrBlockLocale
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays: newRow.arrays,
|
||||
@@ -97,6 +107,7 @@ export const transformArray = ({
|
||||
row: newRow.row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
newRows.push(newRow)
|
||||
|
||||
@@ -26,6 +26,11 @@ type Args = {
|
||||
[tableName: string]: Record<string, unknown>[]
|
||||
}
|
||||
texts: Record<string, unknown>[]
|
||||
/**
|
||||
* Set to a locale code if this set of fields is traversed within a
|
||||
* localized array or block field
|
||||
*/
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
export const transformBlocks = ({
|
||||
adapter,
|
||||
@@ -41,6 +46,7 @@ export const transformBlocks = ({
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
data.forEach((blockRow, i) => {
|
||||
if (typeof blockRow.blockType !== 'string') return
|
||||
@@ -60,6 +66,7 @@ export const transformBlocks = ({
|
||||
}
|
||||
|
||||
if (field.localized && locale) newRow.row._locale = locale
|
||||
if (withinArrayOrBlockLocale) newRow.row._locale = withinArrayOrBlockLocale
|
||||
|
||||
const blockTableName = adapter.tableNameMap.get(`${baseTableName}_blocks_${blockType}`)
|
||||
|
||||
@@ -94,6 +101,7 @@ export const transformBlocks = ({
|
||||
row: newRow.row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
blocks[blockType].push(newRow)
|
||||
|
||||
@@ -58,6 +58,11 @@ type Args = {
|
||||
[tableName: string]: Record<string, unknown>[]
|
||||
}
|
||||
texts: Record<string, unknown>[]
|
||||
/**
|
||||
* Set to a locale code if this set of fields is traversed within a
|
||||
* localized array or block field
|
||||
*/
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
|
||||
export const traverseFields = ({
|
||||
@@ -81,6 +86,7 @@ export const traverseFields = ({
|
||||
row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
fields.forEach((field) => {
|
||||
let columnName = ''
|
||||
@@ -117,6 +123,7 @@ export const traverseFields = ({
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale: localeKey,
|
||||
})
|
||||
|
||||
arrays[arrayTableName] = arrays[arrayTableName].concat(newRows)
|
||||
@@ -138,6 +145,7 @@ export const traverseFields = ({
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
arrays[arrayTableName] = arrays[arrayTableName].concat(newRows)
|
||||
@@ -169,6 +177,7 @@ export const traverseFields = ({
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale: localeKey,
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -187,6 +196,7 @@ export const traverseFields = ({
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -197,6 +207,9 @@ export const traverseFields = ({
|
||||
if (typeof data[field.name] === 'object' && data[field.name] !== null) {
|
||||
if (field.localized) {
|
||||
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
|
||||
// preserve array ID if there is
|
||||
localeData._uuid = data.id || data._uuid
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays,
|
||||
@@ -218,9 +231,14 @@ export const traverseFields = ({
|
||||
row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale: localeKey,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// preserve array ID if there is
|
||||
const groupData = data[field.name] as Record<string, unknown>
|
||||
groupData._uuid = data.id || data._uuid
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays,
|
||||
@@ -228,7 +246,7 @@ export const traverseFields = ({
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
columnPrefix: `${columnName}_`,
|
||||
data: data[field.name] as Record<string, unknown>,
|
||||
data: groupData,
|
||||
existingLocales,
|
||||
fieldPrefix: `${fieldName}_`,
|
||||
fields: field.fields,
|
||||
@@ -241,6 +259,7 @@ export const traverseFields = ({
|
||||
row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -254,6 +273,9 @@ export const traverseFields = ({
|
||||
if (typeof data[tab.name] === 'object' && data[tab.name] !== null) {
|
||||
if (tab.localized) {
|
||||
Object.entries(data[tab.name]).forEach(([localeKey, localeData]) => {
|
||||
// preserve array ID if there is
|
||||
localeData._uuid = data.id || data._uuid
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays,
|
||||
@@ -275,9 +297,14 @@ export const traverseFields = ({
|
||||
row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale: localeKey,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const tabData = data[tab.name] as Record<string, unknown>
|
||||
// preserve array ID if there is
|
||||
tabData._uuid = data.id || data._uuid
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays,
|
||||
@@ -285,7 +312,7 @@ export const traverseFields = ({
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
columnPrefix: `${columnPrefix || ''}${toSnakeCase(tab.name)}_`,
|
||||
data: data[tab.name] as Record<string, unknown>,
|
||||
data: tabData,
|
||||
existingLocales,
|
||||
fieldPrefix: `${fieldPrefix || ''}${tab.name}_`,
|
||||
fields: tab.fields,
|
||||
@@ -298,6 +325,7 @@ export const traverseFields = ({
|
||||
row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -322,6 +350,7 @@ export const traverseFields = ({
|
||||
row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -339,6 +368,7 @@ export const traverseFields = ({
|
||||
existingLocales,
|
||||
fieldPrefix,
|
||||
fields: field.fields,
|
||||
forcedLocale,
|
||||
locales,
|
||||
numbers,
|
||||
parentTableName,
|
||||
@@ -348,6 +378,7 @@ export const traverseFields = ({
|
||||
row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -384,6 +415,7 @@ export const traverseFields = ({
|
||||
|
||||
transformRelationship({
|
||||
baseRow: {
|
||||
locale: withinArrayOrBlockLocale,
|
||||
path: relationshipPath,
|
||||
},
|
||||
data: fieldData,
|
||||
@@ -416,6 +448,7 @@ export const traverseFields = ({
|
||||
} else if (Array.isArray(fieldData)) {
|
||||
transformTexts({
|
||||
baseRow: {
|
||||
locale: withinArrayOrBlockLocale,
|
||||
path: textPath,
|
||||
},
|
||||
data: fieldData,
|
||||
@@ -447,6 +480,7 @@ export const traverseFields = ({
|
||||
} else if (Array.isArray(fieldData)) {
|
||||
transformNumbers({
|
||||
baseRow: {
|
||||
locale: withinArrayOrBlockLocale,
|
||||
path: numberPath,
|
||||
},
|
||||
data: fieldData,
|
||||
@@ -479,6 +513,7 @@ export const traverseFields = ({
|
||||
const newRows = transformSelects({
|
||||
id: data._uuid || data.id,
|
||||
data: data[field.name],
|
||||
locale: withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
selects[selectTableName] = selects[selectTableName].concat(newRows)
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
PgTableWithColumns,
|
||||
PgTransaction,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import type { pgEnum } from 'drizzle-orm/pg-core'
|
||||
import type { PgTableFn } from 'drizzle-orm/pg-core/table'
|
||||
import type { Payload } from 'payload'
|
||||
import type { BaseDatabaseAdapter } from 'payload/database'
|
||||
@@ -60,6 +61,13 @@ export type DrizzleTransaction = PgTransaction<
|
||||
ExtractTablesWithRelations<Record<string, unknown>>
|
||||
>
|
||||
|
||||
type Schema =
|
||||
| {
|
||||
enum: typeof pgEnum
|
||||
table: PgTableFn
|
||||
}
|
||||
| PgSchema
|
||||
|
||||
export type PostgresAdapter = BaseDatabaseAdapter & {
|
||||
drizzle: DrizzleDB
|
||||
enums: Record<string, GenericEnum>
|
||||
@@ -71,13 +79,13 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
|
||||
idType: Args['idType']
|
||||
localesSuffix?: string
|
||||
logger: DrizzleConfig['logger']
|
||||
pgSchema?: { table: PgTableFn } | PgSchema
|
||||
pgSchema?: Schema
|
||||
pool: Pool
|
||||
poolOptions: Args['pool']
|
||||
push: boolean
|
||||
relations: Record<string, GenericRelation>
|
||||
relationshipsSuffix?: string
|
||||
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
|
||||
schema: Record<string, unknown>
|
||||
schemaName?: Args['schemaName']
|
||||
sessions: {
|
||||
[id: string]: {
|
||||
@@ -116,7 +124,7 @@ declare module 'payload' {
|
||||
push: boolean
|
||||
relations: Record<string, GenericRelation>
|
||||
relationshipsSuffix?: string
|
||||
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
|
||||
schema: Record<string, unknown>
|
||||
sessions: {
|
||||
[id: string]: {
|
||||
db: DrizzleTransaction
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { sql } from 'drizzle-orm'
|
||||
|
||||
import type { DrizzleDB } from '../types'
|
||||
import type { PostgresAdapter } from '../types.js'
|
||||
|
||||
export const migrationTableExists = async (db: DrizzleDB): Promise<boolean> => {
|
||||
const queryRes = await db.execute(sql`SELECT to_regclass('public.payload_migrations');`)
|
||||
export const migrationTableExists = async (adapter: PostgresAdapter): Promise<boolean> => {
|
||||
let statement
|
||||
|
||||
// Returns table name 'payload_migrations' or null
|
||||
const exists = queryRes.rows?.[0]?.to_regclass === 'payload_migrations'
|
||||
return exists
|
||||
if (adapter.name === 'postgres') {
|
||||
const prependSchema = adapter.schemaName ? `"${adapter.schemaName}".` : ''
|
||||
statement = `SELECT to_regclass('${prependSchema}"payload_migrations"') AS exists;`
|
||||
}
|
||||
|
||||
const result = await adapter.drizzle.execute(sql.raw(statement))
|
||||
|
||||
const [row] = result.rows
|
||||
|
||||
return row && typeof row === 'object' && 'exists' in row && !!row.exists
|
||||
}
|
||||
|
||||
76
packages/db-postgres/src/utilities/pushDevSchema.ts
Normal file
76
packages/db-postgres/src/utilities/pushDevSchema.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { sql } from 'drizzle-orm'
|
||||
import prompts from 'prompts'
|
||||
|
||||
import type { PostgresAdapter } from '../types.js'
|
||||
|
||||
import { requireDrizzleKit } from './requireDrizzleKit'
|
||||
|
||||
/**
|
||||
* Pushes the development schema to the database using Drizzle.
|
||||
*
|
||||
* @param {PostgresAdapter} adapter - The PostgresAdapter instance connected to the database.
|
||||
* @returns {Promise<void>} - A promise that resolves once the schema push is complete.
|
||||
*/
|
||||
export const pushDevSchema = async (adapter: PostgresAdapter) => {
|
||||
const { pushSchema } = requireDrizzleKit()
|
||||
|
||||
// This will prompt if clarifications are needed for Drizzle to push new schema
|
||||
const { apply, hasDataLoss, warnings } = await pushSchema(
|
||||
adapter.schema,
|
||||
adapter.drizzle,
|
||||
adapter.schemaName ? [adapter.schemaName] : undefined,
|
||||
)
|
||||
|
||||
if (warnings.length) {
|
||||
let message = `Warnings detected during schema push: \n\n${warnings.join('\n')}\n\n`
|
||||
|
||||
if (hasDataLoss) {
|
||||
message += `DATA LOSS WARNING: Possible data loss detected if schema is pushed.\n\n`
|
||||
}
|
||||
|
||||
message += `Accept warnings and push schema to database?`
|
||||
|
||||
const { confirm: acceptWarnings } = await prompts(
|
||||
{
|
||||
name: 'confirm',
|
||||
type: 'confirm',
|
||||
initial: false,
|
||||
message,
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Exit if user does not accept warnings.
|
||||
// Q: Is this the right type of exit for this interaction?
|
||||
if (!acceptWarnings) {
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
await apply()
|
||||
const migrationsTable = adapter.schemaName
|
||||
? `"${adapter.schemaName}"."payload_migrations"`
|
||||
: '"payload_migrations"'
|
||||
|
||||
const { drizzle } = adapter
|
||||
|
||||
const result = await drizzle.execute(
|
||||
sql.raw(`SELECT * FROM ${migrationsTable} WHERE batch = '-1'`),
|
||||
)
|
||||
|
||||
const devPush = result.rows
|
||||
|
||||
if (!devPush.length) {
|
||||
await drizzle.execute(
|
||||
sql.raw(`INSERT INTO ${migrationsTable} (name, batch) VALUES ('dev', '-1')`),
|
||||
)
|
||||
} else {
|
||||
await drizzle.execute(
|
||||
sql.raw(`UPDATE ${migrationsTable} SET updated_at = CURRENT_TIMESTAMP WHERE batch = '-1'`),
|
||||
)
|
||||
}
|
||||
}
|
||||
12
packages/db-postgres/src/utilities/requireDrizzleKit.ts
Normal file
12
packages/db-postgres/src/utilities/requireDrizzleKit.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { PostgresAdapter } from '../types'
|
||||
|
||||
type RequireDrizzleKit = () => {
|
||||
generateDrizzleJson: (args: { schema: Record<string, unknown> }) => unknown
|
||||
pushSchema: (
|
||||
schema: Record<string, unknown>,
|
||||
drizzle: PostgresAdapter['drizzle'],
|
||||
filterSchema?: string[],
|
||||
) => Promise<{ apply; hasDataLoss; warnings }>
|
||||
}
|
||||
|
||||
export const requireDrizzleKit: RequireDrizzleKit = () => require('drizzle-kit/api')
|
||||
@@ -32,7 +32,7 @@
|
||||
"eslint-plugin-perfectionist": "2.0.0",
|
||||
"eslint-plugin-playwright": "0.16.0",
|
||||
"eslint-plugin-react": "7.33.2",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-regexp": "1.15.0"
|
||||
},
|
||||
"keywords": []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "2.29.0",
|
||||
"version": "2.30.3",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
@@ -55,13 +55,13 @@
|
||||
"@date-io/date-fns": "2.16.0",
|
||||
"@dnd-kit/core": "6.0.8",
|
||||
"@dnd-kit/sortable": "7.0.2",
|
||||
"@faceless-ui/modal": "2.0.1",
|
||||
"@faceless-ui/modal": "2.0.2",
|
||||
"@faceless-ui/scroll-info": "1.3.0",
|
||||
"@faceless-ui/window-info": "2.1.1",
|
||||
"@faceless-ui/window-info": "2.1.2",
|
||||
"@monaco-editor/react": "4.5.1",
|
||||
"@swc/core": "1.6.1",
|
||||
"@swc/register": "0.1.10",
|
||||
"body-parser": "1.20.2",
|
||||
"body-parser": "1.20.3",
|
||||
"body-scroll-lock": "4.0.0-beta.0",
|
||||
"bson-objectid": "2.0.4",
|
||||
"compression": "1.7.4",
|
||||
@@ -70,7 +70,7 @@
|
||||
"console-table-printer": "2.11.2",
|
||||
"dataloader": "2.2.2",
|
||||
"date-fns": "2.30.0",
|
||||
"deep-equal": "2.2.2",
|
||||
"deep-equal": "2.2.3",
|
||||
"deepmerge": "4.3.1",
|
||||
"dotenv": "8.6.0",
|
||||
"express": "4.21.0",
|
||||
@@ -97,14 +97,14 @@
|
||||
"isomorphic-fetch": "3.0.0",
|
||||
"joi": "17.9.2",
|
||||
"json-schema-to-typescript": "14.0.5",
|
||||
"jsonwebtoken": "9.0.1",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"jwt-decode": "3.1.2",
|
||||
"md5": "2.3.0",
|
||||
"method-override": "3.0.0",
|
||||
"minimist": "1.2.8",
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.38.0",
|
||||
"nodemailer": "6.9.8",
|
||||
"nodemailer": "6.9.15",
|
||||
"object-to-formdata": "4.5.1",
|
||||
"passport": "0.6.0",
|
||||
"passport-anonymous": "1.0.1",
|
||||
@@ -132,11 +132,11 @@
|
||||
"react-toastify": "10.0.5",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"sass": "1.69.4",
|
||||
"scheduler": "0.23.0",
|
||||
"scheduler": "0.23.2",
|
||||
"scmp": "2.1.0",
|
||||
"sharp": "0.32.6",
|
||||
"swc-loader": "0.2.3",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"swc-loader": "0.2.6",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"ts-essentials": "7.0.3",
|
||||
"use-context-selector": "1.4.1",
|
||||
"uuid": "9.0.1"
|
||||
@@ -145,7 +145,7 @@
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@release-it/conventional-changelog": "7.0.0",
|
||||
"@types/asap": "2.0.0",
|
||||
"@types/body-parser": "1.19.2",
|
||||
"@types/body-parser": "1.19.5",
|
||||
"@types/body-scroll-lock": "^3.1.0",
|
||||
"@types/compression": "1.7.2",
|
||||
"@types/express": "4.17.17",
|
||||
@@ -158,14 +158,14 @@
|
||||
"@types/isomorphic-fetch": "0.0.36",
|
||||
"@types/joi": "14.3.4",
|
||||
"@types/json-schema": "7.0.12",
|
||||
"@types/jsonwebtoken": "8.5.9",
|
||||
"@types/jsonwebtoken": "9.0.7",
|
||||
"@types/method-override": "0.0.32",
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/mini-css-extract-plugin": "^1.4.3",
|
||||
"@types/minimist": "1.2.2",
|
||||
"@types/mkdirp": "1.0.2",
|
||||
"@types/node-fetch": "2.6.4",
|
||||
"@types/nodemailer": "6.4.14",
|
||||
"@types/nodemailer": "6.4.16",
|
||||
"@types/passport": "1.0.12",
|
||||
"@types/passport-anonymous": "1.0.3",
|
||||
"@types/passport-jwt": "3.0.9",
|
||||
@@ -202,9 +202,9 @@
|
||||
"rimraf": "4.4.1",
|
||||
"sass-loader": "12.6.0",
|
||||
"serve-static": "1.15.0",
|
||||
"swc-loader": "^0.2.3",
|
||||
"swc-loader": "^0.2.6",
|
||||
"terser": "5.19.2",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"url-loader": "4.1.1",
|
||||
"vite": "^4.4.9",
|
||||
"webpack": "^5.78.0"
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import ObjectID from 'bson-objectid'
|
||||
|
||||
import { type BeforeDuplicate, type Field, tabHasName } from '../../../../exports/types'
|
||||
|
||||
/**
|
||||
* Creates new IDs for blocks / arrays items to avoid errors with relational databases.
|
||||
*/
|
||||
export const baseBeforeDuplicate = (args: Parameters<BeforeDuplicate>[0]) => {
|
||||
const {
|
||||
collection: { fields },
|
||||
data,
|
||||
} = args
|
||||
|
||||
traverseFields(fields, data)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
function traverseFields(fields: Field[], data: unknown) {
|
||||
if (typeof data === 'undefined' || data === null) return
|
||||
|
||||
fields.forEach((field) => {
|
||||
switch (field.type) {
|
||||
case 'array':
|
||||
if (Array.isArray(data?.[field.name])) {
|
||||
data[field.name].forEach((row) => {
|
||||
if (!row) return
|
||||
row.id = new ObjectID().toHexString()
|
||||
traverseFields(field.fields, row)
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'blocks': {
|
||||
if (Array.isArray(data?.[field.name])) {
|
||||
data[field.name].forEach((row) => {
|
||||
if (!row) return
|
||||
const configBlock = field.blocks.find((block) => block.slug === row.blockType)
|
||||
if (!configBlock) return
|
||||
row.id = new ObjectID().toHexString()
|
||||
traverseFields(configBlock.fields, row)
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'row':
|
||||
case 'collapsible':
|
||||
traverseFields(field.fields, data)
|
||||
break
|
||||
case 'tabs':
|
||||
field.tabs.forEach((tab) => {
|
||||
if (!tabHasName(tab)) {
|
||||
traverseFields(tab.fields, data)
|
||||
return
|
||||
}
|
||||
|
||||
if (data && data[tab.name]) {
|
||||
traverseFields(tab.fields, data[tab.name])
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'group':
|
||||
if (data && data[field.name]) {
|
||||
traverseFields(field.fields, data[field.name])
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import MinimalTemplate from '../../templates/Minimal'
|
||||
import { useConfig } from '../../utilities/Config'
|
||||
import Button from '../Button'
|
||||
import * as PopupList from '../Popup/PopupButtonList'
|
||||
import { baseBeforeDuplicate } from './baseBeforeDuplicate'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'duplicate'
|
||||
@@ -65,6 +66,8 @@ const Duplicate: React.FC<Props> = ({ id, slug, collection }) => {
|
||||
})
|
||||
let data = await response.json()
|
||||
|
||||
data = baseBeforeDuplicate({ collection, data, locale })
|
||||
|
||||
if (typeof collection.admin.hooks?.beforeDuplicate === 'function') {
|
||||
data = await collection.admin.hooks.beforeDuplicate({
|
||||
collection,
|
||||
@@ -73,6 +76,8 @@ const Duplicate: React.FC<Props> = ({ id, slug, collection }) => {
|
||||
})
|
||||
}
|
||||
|
||||
delete data['id']
|
||||
|
||||
if (!duplicateID) {
|
||||
if ('createdAt' in data) delete data.createdAt
|
||||
if ('updatedAt' in data) delete data.updatedAt
|
||||
|
||||
@@ -23,11 +23,13 @@ export {
|
||||
useListDrawer,
|
||||
} from '../../admin/components/elements/ListDrawer'
|
||||
|
||||
export { useNav } from '../../admin/components/elements/Nav/context'
|
||||
|
||||
export { default as NavGroup } from '../../admin/components/elements/NavGroup'
|
||||
export {
|
||||
Description,
|
||||
DescriptionComponent,
|
||||
DescriptionFunction,
|
||||
} from '../../admin/components/forms/FieldDescription/types'
|
||||
|
||||
export { useNav } from '../../admin/components/elements/Nav/context'
|
||||
export { default as NavGroup } from '../../admin/components/elements/NavGroup'
|
||||
export { toast } from 'react-toastify'
|
||||
|
||||
@@ -228,23 +228,58 @@ export const generateFileData = async <T>({
|
||||
withMetadata,
|
||||
})
|
||||
|
||||
filesToSave.push({
|
||||
buffer: croppedImage,
|
||||
path: `${staticPath}/${fsSafeName}`,
|
||||
})
|
||||
// Apply resize after cropping to ensure it conforms to resizeOptions
|
||||
if (resizeOptions) {
|
||||
const resizedAfterCrop = await sharp(croppedImage)
|
||||
.resize({
|
||||
fit: resizeOptions?.fit || 'cover',
|
||||
height: resizeOptions?.height,
|
||||
position: resizeOptions?.position || 'center',
|
||||
width: resizeOptions?.width,
|
||||
})
|
||||
.toBuffer({ resolveWithObject: true })
|
||||
|
||||
fileForResize = {
|
||||
...file,
|
||||
data: croppedImage,
|
||||
size: info.size,
|
||||
filesToSave.push({
|
||||
buffer: resizedAfterCrop.data,
|
||||
path: `${staticPath}/${fsSafeName}`,
|
||||
})
|
||||
|
||||
fileForResize = {
|
||||
...fileForResize,
|
||||
data: resizedAfterCrop.data,
|
||||
size: resizedAfterCrop.info.size,
|
||||
}
|
||||
|
||||
fileData.width = resizedAfterCrop.info.width
|
||||
fileData.height = resizedAfterCrop.info.height
|
||||
if (fileIsAnimatedType) {
|
||||
const metadata = await sharpFile.metadata()
|
||||
fileData.height = metadata.pages
|
||||
? resizedAfterCrop.info.height / metadata.pages
|
||||
: resizedAfterCrop.info.height
|
||||
}
|
||||
fileData.filesize = resizedAfterCrop.info.size
|
||||
} else {
|
||||
// If resizeOptions is not present, just save the cropped image
|
||||
filesToSave.push({
|
||||
buffer: croppedImage,
|
||||
path: `${staticPath}/${fsSafeName}`,
|
||||
})
|
||||
|
||||
fileForResize = {
|
||||
...file,
|
||||
data: croppedImage,
|
||||
size: info.size,
|
||||
}
|
||||
|
||||
fileData.width = info.width
|
||||
fileData.height = info.height
|
||||
if (fileIsAnimatedType) {
|
||||
const metadata = await sharpFile.metadata()
|
||||
fileData.height = metadata.pages ? info.height / metadata.pages : info.height
|
||||
}
|
||||
fileData.filesize = info.size
|
||||
}
|
||||
fileData.width = info.width
|
||||
fileData.height = info.height
|
||||
if (fileIsAnimatedType) {
|
||||
const metadata = await sharpFile.metadata()
|
||||
fileData.height = metadata.pages ? info.height / metadata.pages : info.height
|
||||
}
|
||||
fileData.filesize = info.size
|
||||
|
||||
if (file.tempFilePath) {
|
||||
await fs.promises.writeFile(file.tempFilePath, croppedImage) // write fileBuffer to the temp path
|
||||
|
||||
@@ -191,31 +191,6 @@ const getImageResizeAction = ({
|
||||
return hasFocalPoint ? 'resizeWithFocalPoint' : 'resize'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the image should be passed directly to sharp without payload adjusting properties.
|
||||
*
|
||||
* @param resizeConfig - object containing the requested dimensions and resize options
|
||||
* @param original - the original image size
|
||||
* @returns true if the image should passed directly to sharp
|
||||
*/
|
||||
const applyPayloadAdjustments = (
|
||||
{ fit, height, width, withoutEnlargement, withoutReduction }: ImageSize,
|
||||
original: ProbedImageSize,
|
||||
) => {
|
||||
if (fit === 'contain' || fit === 'inside') return false
|
||||
if (!isNumber(height) && !isNumber(width)) return false
|
||||
|
||||
const targetAspectRatio = width / height
|
||||
const originalAspectRatio = original.width / original.height
|
||||
if (originalAspectRatio === targetAspectRatio) return false
|
||||
|
||||
const skipEnlargement = withoutEnlargement && (original.height < height || original.width < width)
|
||||
const skipReduction = withoutReduction && (original.height > height || original.width > width)
|
||||
if (skipEnlargement || skipReduction) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize the resize config. If the resize config has the `withoutReduction`
|
||||
* property set to true, the `fit` and `position` properties will be set to `contain`
|
||||
@@ -302,6 +277,18 @@ export default async function resizeAndTransformImageSizes({
|
||||
const sharpBase: Sharp | undefined = sharp(file.tempFilePath || file.data, sharpOptions).rotate() // pass rotate() to auto-rotate based on EXIF data. https://github.com/payloadcms/payload/pull/3081
|
||||
const originalImageMeta = await sharpBase.metadata()
|
||||
|
||||
let adjustedDimensions = { ...dimensions }
|
||||
|
||||
// Images with an exif orientation of 5, 6, 7, or 8 are auto-rotated by sharp
|
||||
// Need to adjust the dimensions to match the original image
|
||||
if ([5, 6, 7, 8].includes(originalImageMeta.orientation)) {
|
||||
adjustedDimensions = {
|
||||
...dimensions,
|
||||
height: dimensions.width,
|
||||
width: dimensions.height,
|
||||
}
|
||||
}
|
||||
|
||||
const resizeImageMeta = {
|
||||
height: extractHeightFromImage(originalImageMeta),
|
||||
width: originalImageMeta.width,
|
||||
@@ -324,7 +311,7 @@ export default async function resizeAndTransformImageSizes({
|
||||
if (resizeAction === 'resizeWithFocalPoint') {
|
||||
let { height: resizeHeight, width: resizeWidth } = imageResizeConfig
|
||||
|
||||
const originalAspectRatio = dimensions.width / dimensions.height
|
||||
const originalAspectRatio = adjustedDimensions.width / adjustedDimensions.height
|
||||
|
||||
// Calculate resizeWidth based on original aspect ratio if it's undefined
|
||||
if (resizeHeight && !resizeWidth) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Where } from '../../types'
|
||||
|
||||
export const appendVersionToQueryKey = (query: Where): Where => {
|
||||
export const appendVersionToQueryKey = (query: Where = {}): Where => {
|
||||
return Object.entries(query).reduce((res, [key, val]) => {
|
||||
if (['AND', 'OR', 'and', 'or'].includes(key) && Array.isArray(val)) {
|
||||
return {
|
||||
|
||||
@@ -13,5 +13,9 @@ export const getQueryDraftsSort = (sort: string): string => {
|
||||
orderBy = sort.substring(1)
|
||||
}
|
||||
|
||||
if (orderBy === 'id') {
|
||||
return `${direction}parent`
|
||||
}
|
||||
|
||||
return `${direction}version.${orderBy}`
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import type { Payload } from '../payload'
|
||||
import type { PayloadRequest } from '../types'
|
||||
import type { TypeWithVersion } from './types'
|
||||
|
||||
import { combineQueries } from '../database/combineQueries'
|
||||
import { docHasTimestamps } from '../types'
|
||||
import { appendVersionToQueryKey } from './drafts/appendVersionToQueryKey'
|
||||
|
||||
type Args = {
|
||||
config: SanitizedCollectionConfig
|
||||
@@ -32,7 +34,7 @@ export const getLatestCollectionVersion = async <T extends TypeWithID = any>({
|
||||
pagination: false,
|
||||
req,
|
||||
sort: '-updatedAt',
|
||||
where: { parent: { equals: id } },
|
||||
where: combineQueries(appendVersionToQueryKey(query.where), { parent: { equals: id } }),
|
||||
})
|
||||
;[latestVersion] = docs
|
||||
}
|
||||
|
||||
@@ -96,11 +96,19 @@ From there, create the adapter, passing in all of its required properties:
|
||||
```js
|
||||
import { azureBlobStorageAdapter } from '@payloadcms/plugin-cloud-storage/azure'
|
||||
|
||||
// if you need to obtain credentials you may do so by following the instructions here: https://docs.microsoft.com/en-us/azure/storage/common/storage-auth-aad-app?tabs=javascript
|
||||
// or you can use the connection string directly.
|
||||
|
||||
const adapter = azureBlobStorageAdapter({
|
||||
connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
|
||||
containerName: process.env.AZURE_STORAGE_CONTAINER_NAME,
|
||||
allowContainerCreate: process.env.AZURE_STORAGE_ALLOW_CONTAINER_CREATE === 'true',
|
||||
baseURL: process.env.AZURE_STORAGE_ACCOUNT_BASEURL,
|
||||
/**
|
||||
* Optional: You may wish to obtain credentials that cannot be passed through in the connectionString connection option. In that case the connectionString will only be the URL to the storage account.
|
||||
* Can be one of AnonymousCredential | StorageSharedKeyCredential | TokenCredential
|
||||
**/
|
||||
credentials: new StorageSharedKeyCredential(process.env.AZURE_STORAGE_ACCOUNT_NAME, process.env.AZURE_STORAGE_ACCOUNT_KEY),
|
||||
})
|
||||
|
||||
// Now you can pass this adapter to the plugin
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"version": "1.1.3",
|
||||
"version": "1.2.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
@@ -53,6 +53,7 @@
|
||||
"@aws-sdk/client-s3": "^3.142.0",
|
||||
"@aws-sdk/lib-storage": "^3.267.0",
|
||||
"@azure/storage-blob": "^12.11.0",
|
||||
"@azure/core-http": "^3.0.0",
|
||||
"@google-cloud/storage": "^6.4.1",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/find-node-modules": "^2.1.2",
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { ContainerClient } from '@azure/storage-blob'
|
||||
import type { TokenCredential } from '@azure/core-http'
|
||||
import type {
|
||||
AnonymousCredential,
|
||||
ContainerClient,
|
||||
StorageSharedKeyCredential,
|
||||
} from '@azure/storage-blob'
|
||||
|
||||
import { BlobServiceClient } from '@azure/storage-blob'
|
||||
|
||||
@@ -15,6 +20,7 @@ export interface Args {
|
||||
baseURL: string
|
||||
connectionString: string
|
||||
containerName: string
|
||||
credential?: AnonymousCredential | StorageSharedKeyCredential | TokenCredential
|
||||
}
|
||||
|
||||
export const azureBlobStorageAdapter = ({
|
||||
@@ -22,11 +28,14 @@ export const azureBlobStorageAdapter = ({
|
||||
baseURL,
|
||||
connectionString,
|
||||
containerName,
|
||||
credential,
|
||||
}: Args): Adapter => {
|
||||
let storageClient: ContainerClient | null = null
|
||||
const getStorageClient = () => {
|
||||
if (storageClient) return storageClient
|
||||
const blobServiceClient = BlobServiceClient.fromConnectionString(connectionString)
|
||||
const blobServiceClient = credential
|
||||
? new BlobServiceClient(connectionString, credential)
|
||||
: BlobServiceClient.fromConnectionString(connectionString)
|
||||
return (storageClient = blobServiceClient.getContainerClient(containerName))
|
||||
}
|
||||
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
"@aws-sdk/credential-providers": "^3.289.0",
|
||||
"@aws-sdk/lib-storage": "^3.267.0",
|
||||
"amazon-cognito-identity-js": "^6.1.2",
|
||||
"nodemailer": "6.9.9"
|
||||
"nodemailer": "6.9.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/jest": "^29.5.1",
|
||||
"@types/nodemailer": "6.4.14",
|
||||
"@types/nodemailer": "6.4.16",
|
||||
"payload": "workspace:*",
|
||||
"ts-jest": "^29.1.0",
|
||||
"webpack": "^5.78.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.4",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -22,7 +22,7 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@faceless-ui/modal": "2.0.1",
|
||||
"@faceless-ui/modal": "2.0.2",
|
||||
"@lexical/headless": "0.13.1",
|
||||
"@lexical/link": "0.13.1",
|
||||
"@lexical/list": "0.13.1",
|
||||
@@ -39,7 +39,7 @@
|
||||
"json-schema": "^0.4.0",
|
||||
"lexical": "0.13.1",
|
||||
"lodash": "4.17.21",
|
||||
"react-error-boundary": "4.0.12",
|
||||
"react-error-boundary": "4.0.13",
|
||||
"react-i18next": "11.18.6",
|
||||
"ts-essentials": "7.0.3"
|
||||
},
|
||||
|
||||
@@ -118,13 +118,14 @@ export const LinkFeature = (props: LinkFeatureProps): FeatureProvider => {
|
||||
})
|
||||
|
||||
const rel: string = node.fields.newTab ? ' rel="noopener noreferrer"' : ''
|
||||
const target: string = node.fields.newTab ? ' target="_blank"' : ''
|
||||
|
||||
const href: string =
|
||||
node.fields.linkType === 'custom'
|
||||
? node.fields.url
|
||||
: (node.fields.doc?.value as string)
|
||||
|
||||
return `<a href="${href}"${rel}>${childrenText}</a>`
|
||||
return `<a href="${href}"${target}${rel}>${childrenText}</a>`
|
||||
},
|
||||
nodeTypes: [LinkNode.getType()],
|
||||
} as HTMLConverter<SerializedLinkNode>,
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@faceless-ui/modal": "2.0.1",
|
||||
"@faceless-ui/modal": "2.0.2",
|
||||
"i18next": "22.5.1",
|
||||
"is-hotkey": "0.2.0",
|
||||
"react-i18next": "11.18.6",
|
||||
|
||||
1437
pnpm-lock.yaml
generated
1437
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -177,7 +177,7 @@ If you are migrating an existing site or moving content to a new URL, you can us
|
||||
|
||||
## Website
|
||||
|
||||
This template includes a beautifully designed, production-ready front-end built with the [Next.js App Router](https://nextjs.org), served right alongside your Payload app in a single Express server. This makes is so that you can deploy both apps simultaneously and host them together. If you prefer a different front-end framework, this pattern works for any framework that supports a custom server. If you prefer to host your website separately from Payload, you can easily [Eject](#eject) the front-end out from this template to swap in your own, or to use it as a standalone CMS. For more details, see the official [Custom Server Example](https://github.com/payloadcms/payload/tree/main/examples/custom-server).
|
||||
This template includes a beautifully designed, production-ready front-end built with the [Next.js App Router](https://nextjs.org), served right alongside your Payload app in a single Express server. This makes it so that you can deploy both apps simultaneously and host them together. If you prefer a different front-end framework, this pattern works for any framework that supports a custom server. If you prefer to host your website separately from Payload, you can easily [Eject](#eject) the front-end out from this template to swap in your own, or to use it as a standalone CMS. For more details, see the official [Custom Server Example](https://github.com/payloadcms/payload/tree/main/examples/custom-server).
|
||||
|
||||
Core features:
|
||||
|
||||
|
||||
@@ -877,6 +877,20 @@ describe('collections-rest', () => {
|
||||
expect(result.totalDocs).toEqual(1)
|
||||
})
|
||||
|
||||
it('like - id should not crash', async () => {
|
||||
await createPost({ title: 'post' })
|
||||
|
||||
const response = await client.find({
|
||||
query: {
|
||||
id: {
|
||||
like: 'words partial',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
})
|
||||
|
||||
it('exists - true', async () => {
|
||||
const postWithDesc = await createPost({ description: 'exists' })
|
||||
await createPost({ description: undefined })
|
||||
|
||||
3
test/database/.gitignore
vendored
3
test/database/.gitignore
vendored
@@ -1 +1,4 @@
|
||||
migrations
|
||||
v5_migrations/*
|
||||
!v5_migrations/20241018_162142_test_v5.ts
|
||||
!v5_migrations/20241018_162142_test_v5.json
|
||||
@@ -59,7 +59,7 @@ describe('database', () => {
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
removeFiles(path.normalize(payload.db.migrationDir))
|
||||
removeFiles(path.normalize(payload.db.migrationDir), (name) => !name.includes('test_v5'))
|
||||
})
|
||||
|
||||
it('should run migrate:create', async () => {
|
||||
@@ -74,6 +74,32 @@ describe('database', () => {
|
||||
expect(migrationFile).toContain('_test')
|
||||
})
|
||||
|
||||
it('should run migrate:create with older drizzle version schema', async () => {
|
||||
const db = payload.db as unknown as PostgresAdapter
|
||||
|
||||
// eslint-disable-next-line jest/no-if
|
||||
if (db.name !== 'postgres') return
|
||||
|
||||
// eslint-disable-next-line jest/no-if
|
||||
if (db.schemaName && db.schemaName !== 'public') {
|
||||
return
|
||||
}
|
||||
|
||||
const args = {
|
||||
_: ['migrate:create', 'test'],
|
||||
forceAcceptWarning: true,
|
||||
}
|
||||
const ogMigrationDir = payload.db.migrationDir
|
||||
payload.db.migrationDir = path.resolve(__dirname, 'v5_migrations')
|
||||
await migrate(args)
|
||||
|
||||
// read files names in migrationsDir
|
||||
const migrationFile = path.normalize(fs.readdirSync(payload.db.migrationDir)[2])
|
||||
expect(migrationFile).toContain('_test')
|
||||
removeFiles(path.normalize(payload.db.migrationDir), (name) => !name.includes('test_v5'))
|
||||
payload.db.migrationDir = ogMigrationDir
|
||||
})
|
||||
|
||||
it('should run migrate', async () => {
|
||||
const args = {
|
||||
_: ['migrate'],
|
||||
|
||||
1866
test/database/v5_migrations/20241018_162142_test_v5.json
Normal file
1866
test/database/v5_migrations/20241018_162142_test_v5.json
Normal file
File diff suppressed because it is too large
Load Diff
476
test/database/v5_migrations/20241018_162142_test_v5.ts
Normal file
476
test/database/v5_migrations/20241018_162142_test_v5.ts
Normal file
@@ -0,0 +1,476 @@
|
||||
import { MigrateUpArgs, MigrateDownArgs } from '@payloadcms/db-postgres'
|
||||
import { sql } from 'drizzle-orm'
|
||||
|
||||
export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
await payload.db.drizzle.execute(sql`
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "_locales" AS ENUM('en', 'es');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "selectEnum" AS ENUM('a', 'b', 'c');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "radioEnum" AS ENUM('a', 'b', 'c');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "enum_customs_status" AS ENUM('draft', 'published');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "enum__customs_v_version_status" AS ENUM('draft', 'published');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "posts" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"title" varchar NOT NULL,
|
||||
"throw_after_change" boolean,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "relation_a" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"rich_text" jsonb,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "relation_a_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"relation_b_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "relation_b" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"rich_text" jsonb,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "relation_b_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"relation_a_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "customs_customSelect" (
|
||||
"order" integer NOT NULL,
|
||||
"parent_id" integer NOT NULL,
|
||||
"value" "selectEnum",
|
||||
"id" serial PRIMARY KEY NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "customArrays" (
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
"id" varchar PRIMARY KEY NOT NULL,
|
||||
"text" varchar
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "customArrays_locales" (
|
||||
"localized_text" varchar,
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_locale" "_locales" NOT NULL,
|
||||
"_parent_id" varchar NOT NULL,
|
||||
CONSTRAINT "customArrays_locales_locale_parent_id_unique" UNIQUE("_locale","_parent_id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "customBlocks" (
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
"_path" text NOT NULL,
|
||||
"id" varchar PRIMARY KEY NOT NULL,
|
||||
"text" varchar,
|
||||
"block_name" varchar
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "customBlocks_locales" (
|
||||
"localized_text" varchar,
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_locale" "_locales" NOT NULL,
|
||||
"_parent_id" varchar NOT NULL,
|
||||
CONSTRAINT "customBlocks_locales_locale_parent_id_unique" UNIQUE("_locale","_parent_id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "customs" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"text" varchar,
|
||||
"radio" "radioEnum",
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"_status" "enum_customs_status"
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "customs_locales" (
|
||||
"localized_text" varchar,
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_locale" "_locales" NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
CONSTRAINT "customs_locales_locale_parent_id_unique" UNIQUE("_locale","_parent_id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "customs_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"relation_a_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "_customs_v_version_customSelect" (
|
||||
"order" integer NOT NULL,
|
||||
"parent_id" integer NOT NULL,
|
||||
"value" "selectEnum",
|
||||
"id" serial PRIMARY KEY NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "_customArrays_v" (
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"text" varchar,
|
||||
"_uuid" varchar
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "_customArrays_v_locales" (
|
||||
"localized_text" varchar,
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_locale" "_locales" NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
CONSTRAINT "_customArrays_v_locales_locale_parent_id_unique" UNIQUE("_locale","_parent_id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "_customBlocks_v" (
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
"_path" text NOT NULL,
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"text" varchar,
|
||||
"_uuid" varchar,
|
||||
"block_name" varchar
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "_customBlocks_v_locales" (
|
||||
"localized_text" varchar,
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_locale" "_locales" NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
CONSTRAINT "_customBlocks_v_locales_locale_parent_id_unique" UNIQUE("_locale","_parent_id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "_customs_v" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"version_text" varchar,
|
||||
"version_radio" "radioEnum",
|
||||
"version_updated_at" timestamp(3) with time zone,
|
||||
"version_created_at" timestamp(3) with time zone,
|
||||
"version__status" "enum__customs_v_version_status",
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"latest" boolean
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "_customs_v_locales" (
|
||||
"version_localized_text" varchar,
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_locale" "_locales" NOT NULL,
|
||||
"_parent_id" integer NOT NULL,
|
||||
CONSTRAINT "_customs_v_locales_locale_parent_id_unique" UNIQUE("_locale","_parent_id")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "_customs_v_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"customs_id" integer,
|
||||
"relation_a_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "users" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"email" varchar NOT NULL,
|
||||
"reset_password_token" varchar,
|
||||
"reset_password_expiration" timestamp(3) with time zone,
|
||||
"salt" varchar,
|
||||
"hash" varchar,
|
||||
"login_attempts" numeric,
|
||||
"lock_until" timestamp(3) with time zone
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "payload_preferences" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"key" varchar,
|
||||
"value" jsonb,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "payload_preferences_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"users_id" integer
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "payload_migrations" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" varchar,
|
||||
"batch" numeric,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "customGlobal" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"text" varchar,
|
||||
"updated_at" timestamp(3) with time zone,
|
||||
"created_at" timestamp(3) with time zone
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "_customGlobal_v" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"version_text" varchar,
|
||||
"version_updated_at" timestamp(3) with time zone,
|
||||
"version_created_at" timestamp(3) with time zone,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "posts_created_at_idx" ON "posts" ("created_at");
|
||||
CREATE INDEX IF NOT EXISTS "relation_a_created_at_idx" ON "relation_a" ("created_at");
|
||||
CREATE INDEX IF NOT EXISTS "relation_a_rels_order_idx" ON "relation_a_rels" ("order");
|
||||
CREATE INDEX IF NOT EXISTS "relation_a_rels_parent_idx" ON "relation_a_rels" ("parent_id");
|
||||
CREATE INDEX IF NOT EXISTS "relation_a_rels_path_idx" ON "relation_a_rels" ("path");
|
||||
CREATE INDEX IF NOT EXISTS "relation_b_created_at_idx" ON "relation_b" ("created_at");
|
||||
CREATE INDEX IF NOT EXISTS "relation_b_rels_order_idx" ON "relation_b_rels" ("order");
|
||||
CREATE INDEX IF NOT EXISTS "relation_b_rels_parent_idx" ON "relation_b_rels" ("parent_id");
|
||||
CREATE INDEX IF NOT EXISTS "relation_b_rels_path_idx" ON "relation_b_rels" ("path");
|
||||
CREATE INDEX IF NOT EXISTS "customs_customSelect_order_idx" ON "customs_customSelect" ("order");
|
||||
CREATE INDEX IF NOT EXISTS "customs_customSelect_parent_idx" ON "customs_customSelect" ("parent_id");
|
||||
CREATE INDEX IF NOT EXISTS "customArrays_order_idx" ON "customArrays" ("_order");
|
||||
CREATE INDEX IF NOT EXISTS "customArrays_parent_id_idx" ON "customArrays" ("_parent_id");
|
||||
CREATE INDEX IF NOT EXISTS "customBlocks_order_idx" ON "customBlocks" ("_order");
|
||||
CREATE INDEX IF NOT EXISTS "customBlocks_parent_id_idx" ON "customBlocks" ("_parent_id");
|
||||
CREATE INDEX IF NOT EXISTS "customBlocks_path_idx" ON "customBlocks" ("_path");
|
||||
CREATE INDEX IF NOT EXISTS "customs_created_at_idx" ON "customs" ("created_at");
|
||||
CREATE INDEX IF NOT EXISTS "customs__status_idx" ON "customs" ("_status");
|
||||
CREATE INDEX IF NOT EXISTS "customs_rels_order_idx" ON "customs_rels" ("order");
|
||||
CREATE INDEX IF NOT EXISTS "customs_rels_parent_idx" ON "customs_rels" ("parent_id");
|
||||
CREATE INDEX IF NOT EXISTS "customs_rels_path_idx" ON "customs_rels" ("path");
|
||||
CREATE INDEX IF NOT EXISTS "_customs_v_version_customSelect_order_idx" ON "_customs_v_version_customSelect" ("order");
|
||||
CREATE INDEX IF NOT EXISTS "_customs_v_version_customSelect_parent_idx" ON "_customs_v_version_customSelect" ("parent_id");
|
||||
CREATE INDEX IF NOT EXISTS "_customArrays_v_order_idx" ON "_customArrays_v" ("_order");
|
||||
CREATE INDEX IF NOT EXISTS "_customArrays_v_parent_id_idx" ON "_customArrays_v" ("_parent_id");
|
||||
CREATE INDEX IF NOT EXISTS "_customBlocks_v_order_idx" ON "_customBlocks_v" ("_order");
|
||||
CREATE INDEX IF NOT EXISTS "_customBlocks_v_parent_id_idx" ON "_customBlocks_v" ("_parent_id");
|
||||
CREATE INDEX IF NOT EXISTS "_customBlocks_v_path_idx" ON "_customBlocks_v" ("_path");
|
||||
CREATE INDEX IF NOT EXISTS "_customs_v_version_version_created_at_idx" ON "_customs_v" ("version_created_at");
|
||||
CREATE INDEX IF NOT EXISTS "_customs_v_version_version__status_idx" ON "_customs_v" ("version__status");
|
||||
CREATE INDEX IF NOT EXISTS "_customs_v_created_at_idx" ON "_customs_v" ("created_at");
|
||||
CREATE INDEX IF NOT EXISTS "_customs_v_updated_at_idx" ON "_customs_v" ("updated_at");
|
||||
CREATE INDEX IF NOT EXISTS "_customs_v_latest_idx" ON "_customs_v" ("latest");
|
||||
CREATE INDEX IF NOT EXISTS "_customs_v_rels_order_idx" ON "_customs_v_rels" ("order");
|
||||
CREATE INDEX IF NOT EXISTS "_customs_v_rels_parent_idx" ON "_customs_v_rels" ("parent_id");
|
||||
CREATE INDEX IF NOT EXISTS "_customs_v_rels_path_idx" ON "_customs_v_rels" ("path");
|
||||
CREATE INDEX IF NOT EXISTS "users_created_at_idx" ON "users" ("created_at");
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "users_email_idx" ON "users" ("email");
|
||||
CREATE INDEX IF NOT EXISTS "payload_preferences_key_idx" ON "payload_preferences" ("key");
|
||||
CREATE INDEX IF NOT EXISTS "payload_preferences_created_at_idx" ON "payload_preferences" ("created_at");
|
||||
CREATE INDEX IF NOT EXISTS "payload_preferences_rels_order_idx" ON "payload_preferences_rels" ("order");
|
||||
CREATE INDEX IF NOT EXISTS "payload_preferences_rels_parent_idx" ON "payload_preferences_rels" ("parent_id");
|
||||
CREATE INDEX IF NOT EXISTS "payload_preferences_rels_path_idx" ON "payload_preferences_rels" ("path");
|
||||
CREATE INDEX IF NOT EXISTS "payload_migrations_created_at_idx" ON "payload_migrations" ("created_at");
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "relation_a_rels" ADD CONSTRAINT "relation_a_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "relation_a"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "relation_a_rels" ADD CONSTRAINT "relation_a_rels_relation_b_fk" FOREIGN KEY ("relation_b_id") REFERENCES "relation_b"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "relation_b_rels" ADD CONSTRAINT "relation_b_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "relation_b"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "relation_b_rels" ADD CONSTRAINT "relation_b_rels_relation_a_fk" FOREIGN KEY ("relation_a_id") REFERENCES "relation_a"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "customs_customSelect" ADD CONSTRAINT "customs_customSelect_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "customs"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "customArrays" ADD CONSTRAINT "customArrays_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "customs"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "customArrays_locales" ADD CONSTRAINT "customArrays_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "customArrays"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "customBlocks" ADD CONSTRAINT "customBlocks_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "customs"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "customBlocks_locales" ADD CONSTRAINT "customBlocks_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "customBlocks"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "customs_locales" ADD CONSTRAINT "customs_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "customs"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "customs_rels" ADD CONSTRAINT "customs_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "customs"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "customs_rels" ADD CONSTRAINT "customs_rels_relation_a_fk" FOREIGN KEY ("relation_a_id") REFERENCES "relation_a"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_customs_v_version_customSelect" ADD CONSTRAINT "_customs_v_version_customSelect_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "_customs_v"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_customArrays_v" ADD CONSTRAINT "_customArrays_v_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "_customs_v"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_customArrays_v_locales" ADD CONSTRAINT "_customArrays_v_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "_customArrays_v"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_customBlocks_v" ADD CONSTRAINT "_customBlocks_v_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "_customs_v"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_customBlocks_v_locales" ADD CONSTRAINT "_customBlocks_v_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "_customBlocks_v"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_customs_v_locales" ADD CONSTRAINT "_customs_v_locales_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "_customs_v"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_customs_v_rels" ADD CONSTRAINT "_customs_v_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "_customs_v"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_customs_v_rels" ADD CONSTRAINT "_customs_v_rels_custom_schema_fk" FOREIGN KEY ("customs_id") REFERENCES "customs"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_customs_v_rels" ADD CONSTRAINT "_customs_v_rels_relation_a_fk" FOREIGN KEY ("relation_a_id") REFERENCES "relation_a"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "payload_preferences"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
`)
|
||||
}
|
||||
|
||||
export async function down({ payload }: MigrateDownArgs): Promise<void> {
|
||||
await payload.db.drizzle.execute(sql`
|
||||
|
||||
DROP TABLE "posts";
|
||||
DROP TABLE "relation_a";
|
||||
DROP TABLE "relation_a_rels";
|
||||
DROP TABLE "relation_b";
|
||||
DROP TABLE "relation_b_rels";
|
||||
DROP TABLE "customs_customSelect";
|
||||
DROP TABLE "customArrays";
|
||||
DROP TABLE "customArrays_locales";
|
||||
DROP TABLE "customBlocks";
|
||||
DROP TABLE "customBlocks_locales";
|
||||
DROP TABLE "customs";
|
||||
DROP TABLE "customs_locales";
|
||||
DROP TABLE "customs_rels";
|
||||
DROP TABLE "_customs_v_version_customSelect";
|
||||
DROP TABLE "_customArrays_v";
|
||||
DROP TABLE "_customArrays_v_locales";
|
||||
DROP TABLE "_customBlocks_v";
|
||||
DROP TABLE "_customBlocks_v_locales";
|
||||
DROP TABLE "_customs_v";
|
||||
DROP TABLE "_customs_v_locales";
|
||||
DROP TABLE "_customs_v_rels";
|
||||
DROP TABLE "users";
|
||||
DROP TABLE "payload_preferences";
|
||||
DROP TABLE "payload_preferences_rels";
|
||||
DROP TABLE "payload_migrations";
|
||||
DROP TABLE "customGlobal";
|
||||
DROP TABLE "_customGlobal_v";`)
|
||||
}
|
||||
@@ -227,6 +227,86 @@ const GroupFields: CollectionConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'localizedGroupArr',
|
||||
type: 'group',
|
||||
localized: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'localizedGroupSelect',
|
||||
type: 'group',
|
||||
localized: true,
|
||||
fields: [
|
||||
{
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: ['one', 'two'],
|
||||
name: 'select',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'localizedGroupRel',
|
||||
type: 'group',
|
||||
localized: true,
|
||||
fields: [
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: 'text-fields',
|
||||
name: 'rel',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'localizedGroupManyRel',
|
||||
type: 'group',
|
||||
localized: true,
|
||||
fields: [
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: 'text-fields',
|
||||
name: 'email',
|
||||
hasMany: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'localizedGroupPolyRel',
|
||||
type: 'group',
|
||||
localized: true,
|
||||
fields: [
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: ['text-fields'],
|
||||
name: 'email',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'localizedGroupPolyHasManyRel',
|
||||
type: 'group',
|
||||
localized: true,
|
||||
fields: [
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: ['text-fields'],
|
||||
name: 'email',
|
||||
hasMany: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,88 @@ const SelectFields: CollectionConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'selectHasMany',
|
||||
hasMany: true,
|
||||
type: 'select',
|
||||
admin: {
|
||||
isClearable: true,
|
||||
isSortable: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: 'Value One',
|
||||
value: 'one',
|
||||
},
|
||||
{
|
||||
label: 'Value Two',
|
||||
value: 'two',
|
||||
},
|
||||
{
|
||||
label: 'Value Three',
|
||||
value: 'three',
|
||||
},
|
||||
{
|
||||
label: 'Value Four',
|
||||
value: 'four',
|
||||
},
|
||||
{
|
||||
label: 'Value Five',
|
||||
value: 'five',
|
||||
},
|
||||
{
|
||||
label: 'Value Six',
|
||||
value: 'six',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'selectHasMany',
|
||||
hasMany: true,
|
||||
type: 'select',
|
||||
admin: {
|
||||
isClearable: true,
|
||||
isSortable: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: 'Value One',
|
||||
value: 'one',
|
||||
},
|
||||
{
|
||||
label: 'Value Two',
|
||||
value: 'two',
|
||||
},
|
||||
{
|
||||
label: 'Value Three',
|
||||
value: 'three',
|
||||
},
|
||||
{
|
||||
label: 'Value Four',
|
||||
value: 'four',
|
||||
},
|
||||
{
|
||||
label: 'Value Five',
|
||||
value: 'five',
|
||||
},
|
||||
{
|
||||
label: 'Value Six',
|
||||
value: 'six',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'selectHasManyLocalized',
|
||||
type: 'select',
|
||||
|
||||
@@ -573,6 +573,54 @@ describe('Fields', () => {
|
||||
|
||||
expect(resInSecond.totalDocs).toBe(1)
|
||||
})
|
||||
|
||||
it('should CRUD within array hasMany', async () => {
|
||||
const doc = await payload.create({
|
||||
collection: 'select-fields',
|
||||
data: { array: [{ selectHasMany: ['one', 'two'] }] },
|
||||
})
|
||||
|
||||
expect(doc.array[0].selectHasMany).toStrictEqual(['one', 'two'])
|
||||
|
||||
const upd = await payload.update({
|
||||
collection: 'select-fields',
|
||||
id: doc.id,
|
||||
data: {
|
||||
array: [
|
||||
{
|
||||
id: doc.array[0].id,
|
||||
selectHasMany: ['six'],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(upd.array[0].selectHasMany).toStrictEqual(['six'])
|
||||
})
|
||||
|
||||
it('should CRUD within array + group hasMany', async () => {
|
||||
const doc = await payload.create({
|
||||
collection: 'select-fields',
|
||||
data: { array: [{ group: { selectHasMany: ['one', 'two'] } }] },
|
||||
})
|
||||
|
||||
expect(doc.array[0].group.selectHasMany).toStrictEqual(['one', 'two'])
|
||||
|
||||
const upd = await payload.update({
|
||||
collection: 'select-fields',
|
||||
id: doc.id,
|
||||
data: {
|
||||
array: [
|
||||
{
|
||||
id: doc.array[0].id,
|
||||
group: { selectHasMany: ['six'] },
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(upd.array[0].group.selectHasMany).toStrictEqual(['six'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('number', () => {
|
||||
@@ -842,6 +890,37 @@ describe('Fields', () => {
|
||||
|
||||
expect(resInSecond.totalDocs).toBe(1)
|
||||
})
|
||||
|
||||
it('should properly query numbers with exists operator', async () => {
|
||||
await payload.create({
|
||||
collection: 'number-fields',
|
||||
data: {
|
||||
number: null,
|
||||
},
|
||||
})
|
||||
|
||||
const numbersExist = await payload.find({
|
||||
collection: 'number-fields',
|
||||
where: {
|
||||
number: {
|
||||
exists: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(numbersExist.totalDocs).toBe(4)
|
||||
|
||||
const numbersNotExists = await payload.find({
|
||||
collection: 'number-fields',
|
||||
where: {
|
||||
number: {
|
||||
exists: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(numbersNotExists.docs).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
if (isMongoose(payload)) {
|
||||
@@ -1299,6 +1378,324 @@ describe('Fields', () => {
|
||||
expect(res.camelCaseGroup.array[0].array[0].text).toBe('nested')
|
||||
expect(res.camelCaseGroup.nesGroup.arr[0].text).toBe('nestedCamel')
|
||||
})
|
||||
|
||||
it('should insert/update/read localized group with array inside', async () => {
|
||||
const doc = await payload.create({
|
||||
collection: 'group-fields',
|
||||
locale: 'en',
|
||||
data: {
|
||||
group: { text: 'req' },
|
||||
localizedGroupArr: {
|
||||
array: [{ text: 'text-en' }],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(doc.localizedGroupArr.array[0].text).toBe('text-en')
|
||||
|
||||
const esDoc = await payload.update({
|
||||
collection: 'group-fields',
|
||||
locale: 'es',
|
||||
id: doc.id,
|
||||
data: {
|
||||
localizedGroupArr: {
|
||||
array: [{ text: 'text-es' }],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(esDoc.localizedGroupArr.array[0].text).toBe('text-es')
|
||||
|
||||
const allDoc = await payload.findByID({
|
||||
collection: 'group-fields',
|
||||
id: doc.id,
|
||||
locale: 'all',
|
||||
})
|
||||
|
||||
expect(allDoc.localizedGroupArr.en.array[0].text).toBe('text-en')
|
||||
expect(allDoc.localizedGroupArr.es.array[0].text).toBe('text-es')
|
||||
})
|
||||
|
||||
it('should insert/update/read localized group with select hasMany inside', async () => {
|
||||
const doc = await payload.create({
|
||||
collection: 'group-fields',
|
||||
locale: 'en',
|
||||
data: {
|
||||
group: { text: 'req' },
|
||||
localizedGroupSelect: {
|
||||
select: ['one', 'two'],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(doc.localizedGroupSelect.select).toStrictEqual(['one', 'two'])
|
||||
|
||||
const esDoc = await payload.update({
|
||||
collection: 'group-fields',
|
||||
locale: 'es',
|
||||
id: doc.id,
|
||||
data: {
|
||||
localizedGroupSelect: {
|
||||
select: ['one'],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(esDoc.localizedGroupSelect.select).toStrictEqual(['one'])
|
||||
|
||||
const allDoc = await payload.findByID({
|
||||
collection: 'group-fields',
|
||||
id: doc.id,
|
||||
locale: 'all',
|
||||
})
|
||||
|
||||
expect(allDoc.localizedGroupSelect.en.select).toStrictEqual(['one', 'two'])
|
||||
expect(allDoc.localizedGroupSelect.es.select).toStrictEqual(['one'])
|
||||
})
|
||||
|
||||
it('should insert/update/read localized group with relationship inside', async () => {
|
||||
const rel_1 = await payload.create({
|
||||
collection: 'text-fields',
|
||||
data: { text: 'pro123@gmail.com' },
|
||||
})
|
||||
|
||||
const rel_2 = await payload.create({
|
||||
collection: 'text-fields',
|
||||
data: { text: 'frank@gmail.com' },
|
||||
})
|
||||
|
||||
const doc = await payload.create({
|
||||
collection: 'group-fields',
|
||||
depth: 0,
|
||||
data: {
|
||||
group: { text: 'requireddd' },
|
||||
localizedGroupRel: {
|
||||
rel: rel_1.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(doc.localizedGroupRel.rel).toBe(rel_1.id)
|
||||
|
||||
const upd = await payload.update({
|
||||
collection: 'group-fields',
|
||||
depth: 0,
|
||||
id: doc.id,
|
||||
locale: 'es',
|
||||
data: {
|
||||
localizedGroupRel: {
|
||||
rel: rel_2.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(upd.localizedGroupRel.rel).toBe(rel_2.id)
|
||||
|
||||
const docAll = await payload.findByID({
|
||||
collection: 'group-fields',
|
||||
id: doc.id,
|
||||
locale: 'all',
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
expect(docAll.localizedGroupRel.en.rel).toBe(rel_1.id)
|
||||
expect(docAll.localizedGroupRel.es.rel).toBe(rel_2.id)
|
||||
})
|
||||
|
||||
it('should insert/update/read localized group with hasMany relationship inside', async () => {
|
||||
const rel_1 = await payload.create({
|
||||
collection: 'text-fields',
|
||||
data: { text: 'pro123@gmail.com' },
|
||||
})
|
||||
|
||||
const rel_2 = await payload.create({
|
||||
collection: 'text-fields',
|
||||
data: { text: 'frank@gmail.com' },
|
||||
})
|
||||
|
||||
const doc = await payload.create({
|
||||
collection: 'group-fields',
|
||||
depth: 0,
|
||||
data: {
|
||||
group: { text: 'requireddd' },
|
||||
localizedGroupManyRel: {
|
||||
email: [rel_1.id],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(doc.localizedGroupManyRel.email).toStrictEqual([rel_1.id])
|
||||
|
||||
const upd = await payload.update({
|
||||
collection: 'group-fields',
|
||||
depth: 0,
|
||||
id: doc.id,
|
||||
locale: 'es',
|
||||
data: {
|
||||
localizedGroupManyRel: {
|
||||
email: [rel_2.id],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(upd.localizedGroupManyRel.email).toStrictEqual([rel_2.id])
|
||||
|
||||
const docAll = await payload.findByID({
|
||||
collection: 'group-fields',
|
||||
id: doc.id,
|
||||
locale: 'all',
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
expect(docAll.localizedGroupManyRel.en.email).toStrictEqual([rel_1.id])
|
||||
expect(docAll.localizedGroupManyRel.es.email).toStrictEqual([rel_2.id])
|
||||
})
|
||||
|
||||
it('should insert/update/read localized group with poly relationship inside', async () => {
|
||||
const rel_1 = await payload.create({
|
||||
collection: 'text-fields',
|
||||
data: { text: 'pro123@gmail.com' },
|
||||
})
|
||||
|
||||
const rel_2 = await payload.create({
|
||||
collection: 'text-fields',
|
||||
data: { text: 'frank@gmail.com' },
|
||||
})
|
||||
|
||||
const doc = await payload.create({
|
||||
collection: 'group-fields',
|
||||
depth: 0,
|
||||
data: {
|
||||
group: { text: 'requireddd' },
|
||||
localizedGroupPolyRel: {
|
||||
email: {
|
||||
relationTo: 'text-fields',
|
||||
value: rel_1.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(doc.localizedGroupPolyRel.email).toStrictEqual({
|
||||
relationTo: 'text-fields',
|
||||
value: rel_1.id,
|
||||
})
|
||||
|
||||
const upd = await payload.update({
|
||||
collection: 'group-fields',
|
||||
depth: 0,
|
||||
id: doc.id,
|
||||
locale: 'es',
|
||||
data: {
|
||||
localizedGroupPolyRel: {
|
||||
email: {
|
||||
value: rel_2.id,
|
||||
relationTo: 'text-fields',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(upd.localizedGroupPolyRel.email).toStrictEqual({
|
||||
value: rel_2.id,
|
||||
relationTo: 'text-fields',
|
||||
})
|
||||
|
||||
const docAll = await payload.findByID({
|
||||
collection: 'group-fields',
|
||||
id: doc.id,
|
||||
locale: 'all',
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
expect(docAll.localizedGroupPolyRel.en.email).toStrictEqual({
|
||||
value: rel_1.id,
|
||||
relationTo: 'text-fields',
|
||||
})
|
||||
expect(docAll.localizedGroupPolyRel.es.email).toStrictEqual({
|
||||
value: rel_2.id,
|
||||
relationTo: 'text-fields',
|
||||
})
|
||||
})
|
||||
|
||||
it('should insert/update/read localized group with poly hasMany relationship inside', async () => {
|
||||
const rel_1 = await payload.create({
|
||||
collection: 'text-fields',
|
||||
data: { text: 'pro123@gmail.com' },
|
||||
})
|
||||
|
||||
const rel_2 = await payload.create({
|
||||
collection: 'text-fields',
|
||||
data: { text: 'frank@gmail.com' },
|
||||
})
|
||||
|
||||
const doc = await payload.create({
|
||||
collection: 'group-fields',
|
||||
depth: 0,
|
||||
data: {
|
||||
group: { text: 'requireddd' },
|
||||
localizedGroupPolyHasManyRel: {
|
||||
email: [
|
||||
{
|
||||
relationTo: 'text-fields',
|
||||
value: rel_1.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(doc.localizedGroupPolyHasManyRel.email).toStrictEqual([
|
||||
{
|
||||
relationTo: 'text-fields',
|
||||
value: rel_1.id,
|
||||
},
|
||||
])
|
||||
|
||||
const upd = await payload.update({
|
||||
collection: 'group-fields',
|
||||
depth: 0,
|
||||
id: doc.id,
|
||||
locale: 'es',
|
||||
data: {
|
||||
localizedGroupPolyHasManyRel: {
|
||||
email: [
|
||||
{
|
||||
value: rel_2.id,
|
||||
relationTo: 'text-fields',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(upd.localizedGroupPolyHasManyRel.email).toStrictEqual([
|
||||
{
|
||||
value: rel_2.id,
|
||||
relationTo: 'text-fields',
|
||||
},
|
||||
])
|
||||
|
||||
const docAll = await payload.findByID({
|
||||
collection: 'group-fields',
|
||||
id: doc.id,
|
||||
locale: 'all',
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
expect(docAll.localizedGroupPolyHasManyRel.en.email).toStrictEqual([
|
||||
{
|
||||
value: rel_1.id,
|
||||
relationTo: 'text-fields',
|
||||
},
|
||||
])
|
||||
expect(docAll.localizedGroupPolyHasManyRel.es.email).toStrictEqual([
|
||||
{
|
||||
value: rel_2.id,
|
||||
relationTo: 'text-fields',
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('tabs', () => {
|
||||
|
||||
@@ -271,6 +271,17 @@ export interface ArrayField {
|
||||
id?: string | null
|
||||
}[]
|
||||
| null
|
||||
nestedArrayLocalized?:
|
||||
| {
|
||||
array?:
|
||||
| {
|
||||
text?: string | null
|
||||
id?: string | null
|
||||
}[]
|
||||
| null
|
||||
id?: string | null
|
||||
}[]
|
||||
| null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
@@ -841,6 +852,37 @@ export interface GroupField {
|
||||
| null
|
||||
}
|
||||
}
|
||||
localizedGroupArr?: {
|
||||
array?:
|
||||
| {
|
||||
text?: string | null
|
||||
id?: string | null
|
||||
}[]
|
||||
| null
|
||||
}
|
||||
localizedGroupSelect?: {
|
||||
select?: ('one' | 'two')[] | null
|
||||
}
|
||||
localizedGroupRel?: {
|
||||
rel?: (string | null) | TextField
|
||||
}
|
||||
localizedGroupManyRel?: {
|
||||
email?: (string | TextField)[] | null
|
||||
}
|
||||
localizedGroupPolyRel?: {
|
||||
email?: {
|
||||
relationTo: 'text-fields'
|
||||
value: string | TextField
|
||||
} | null
|
||||
}
|
||||
localizedGroupPolyHasManyRel?: {
|
||||
email?:
|
||||
| {
|
||||
relationTo: 'text-fields'
|
||||
value: string | TextField
|
||||
}[]
|
||||
| null
|
||||
}
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
@@ -1116,6 +1158,15 @@ export interface SelectField {
|
||||
select?: ('one' | 'two' | 'three') | null
|
||||
selectReadOnly?: ('one' | 'two' | 'three') | null
|
||||
selectHasMany?: ('one' | 'two' | 'three' | 'four' | 'five' | 'six')[] | null
|
||||
array?:
|
||||
| {
|
||||
selectHasMany?: ('one' | 'two' | 'three' | 'four' | 'five' | 'six')[] | null
|
||||
group?: {
|
||||
selectHasMany?: ('one' | 'two' | 'three' | 'four' | 'five' | 'six')[] | null
|
||||
}
|
||||
id?: string | null
|
||||
}[]
|
||||
| null
|
||||
selectHasManyLocalized?: ('one' | 'two')[] | null
|
||||
selectI18n?: ('one' | 'two' | 'three') | null
|
||||
simple?: ('One' | 'Two' | 'Three') | null
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import fs from 'fs'
|
||||
|
||||
const removeFiles = (dir) => {
|
||||
const removeFiles = (dir, nameFilter?: (name: string) => boolean) => {
|
||||
if (!fs.existsSync(dir)) return
|
||||
|
||||
fs.readdirSync(dir).forEach((f) => {
|
||||
if (nameFilter && !nameFilter(f)) return
|
||||
return fs.rmSync(`${dir}/${f}`, { recursive: true })
|
||||
})
|
||||
}
|
||||
|
||||
37
test/localization/collections/Blocks/index.ts
Normal file
37
test/localization/collections/Blocks/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
|
||||
export const blocksCollectionSlug = 'blocks-fields'
|
||||
|
||||
export const BlocksCollection: CollectionConfig = {
|
||||
slug: blocksCollectionSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
label: 'Content',
|
||||
type: 'blocks',
|
||||
localized: true,
|
||||
blocks: [
|
||||
{
|
||||
slug: 'blockInsideBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'textBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
81
test/localization/collections/Group/index.ts
Normal file
81
test/localization/collections/Group/index.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const groupSlug = 'groups'
|
||||
|
||||
export const Group: CollectionConfig = {
|
||||
slug: groupSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'groupLocalizedRow',
|
||||
type: 'group',
|
||||
localized: true,
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'groupLocalized',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
localized: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'deep',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'blocks',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'first',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
42
test/localization/collections/NestedArray/index.ts
Normal file
42
test/localization/collections/NestedArray/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
|
||||
export const NestedArray: CollectionConfig = {
|
||||
slug: 'nested-arrays',
|
||||
fields: [
|
||||
{
|
||||
name: 'arrayWithBlocks',
|
||||
type: 'array',
|
||||
localized: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'blocksWithinArray',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'someBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'relationWithinBlock',
|
||||
type: 'relationship',
|
||||
relationTo: 'localized-posts',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'arrayWithLocalizedRelation',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'localizedRelation',
|
||||
type: 'relationship',
|
||||
localized: true,
|
||||
relationTo: 'localized-posts',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
86
test/localization/collections/NestedFields/index.ts
Normal file
86
test/localization/collections/NestedFields/index.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
|
||||
export const NestedFields: CollectionConfig = {
|
||||
slug: 'nested-field-tables',
|
||||
fields: [
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
localized: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'relation',
|
||||
type: 'relationship',
|
||||
relationTo: ['localized-posts'],
|
||||
},
|
||||
{
|
||||
name: 'hasManyRelation',
|
||||
type: 'relationship',
|
||||
hasMany: true,
|
||||
relationTo: 'localized-posts',
|
||||
},
|
||||
{
|
||||
name: 'hasManyPolyRelation',
|
||||
type: 'relationship',
|
||||
hasMany: true,
|
||||
relationTo: ['localized-posts'],
|
||||
},
|
||||
{
|
||||
name: 'select',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: ['one', 'two', 'three'],
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
type: 'number',
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
hasMany: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'blocks',
|
||||
type: 'blocks',
|
||||
localized: true,
|
||||
blocks: [
|
||||
{
|
||||
slug: 'block',
|
||||
fields: [
|
||||
{
|
||||
name: 'nestedBlocks',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'content',
|
||||
fields: [
|
||||
{
|
||||
name: 'relation',
|
||||
type: 'relationship',
|
||||
relationTo: ['localized-posts'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'relation',
|
||||
type: 'relationship',
|
||||
relationTo: ['localized-posts'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
66
test/localization/collections/Tab/index.ts
Normal file
66
test/localization/collections/Tab/index.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
|
||||
export const tabSlug = 'tabs'
|
||||
|
||||
export const Tab: CollectionConfig = {
|
||||
slug: tabSlug,
|
||||
fields: [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
name: 'tabLocalized',
|
||||
localized: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'tab',
|
||||
fields: [
|
||||
{
|
||||
localized: true,
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'deep',
|
||||
fields: [
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
localized: true,
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'blocks',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'first',
|
||||
fields: [
|
||||
{
|
||||
localized: true,
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -4,8 +4,13 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults'
|
||||
import { devUser } from '../credentials'
|
||||
import { englishLocale } from '../globals/config'
|
||||
import { ArrayCollection } from './collections/Array'
|
||||
import { BlocksCollection } from './collections/Blocks'
|
||||
import { Group } from './collections/Group'
|
||||
import { NestedArray } from './collections/NestedArray'
|
||||
import { NestedFields } from './collections/NestedFields'
|
||||
import { NestedToArrayAndBlock } from './collections/NestedToArrayAndBlock'
|
||||
import { RestrictedByLocaleCollection } from './collections/RestrictedByLocale'
|
||||
import { Tab } from './collections/Tab'
|
||||
import {
|
||||
blocksWithLocalizedSameName,
|
||||
defaultLocale,
|
||||
@@ -41,6 +46,9 @@ const openAccess = {
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [
|
||||
BlocksCollection,
|
||||
NestedArray,
|
||||
NestedFields,
|
||||
{
|
||||
auth: true,
|
||||
fields: [
|
||||
@@ -127,6 +135,16 @@ export default buildConfigWithDefaults({
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'nestedArray',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
slug: 'text',
|
||||
},
|
||||
@@ -144,6 +162,41 @@ export default buildConfigWithDefaults({
|
||||
required: true,
|
||||
type: 'blocks',
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
name: 'myTab',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
localized: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'nestedArray2',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'nestedText',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'nestedText',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
slug: withRequiredLocalizedFields,
|
||||
},
|
||||
@@ -235,6 +288,8 @@ export default buildConfigWithDefaults({
|
||||
slug: 'dummy',
|
||||
},
|
||||
NestedToArrayAndBlock,
|
||||
Group,
|
||||
Tab,
|
||||
{
|
||||
slug: localizedSortSlug,
|
||||
access: openAccess,
|
||||
|
||||
@@ -11,7 +11,9 @@ import { initPayloadTest } from '../helpers/configHelpers'
|
||||
import { idToString } from '../helpers/idToString'
|
||||
import { RESTClient } from '../helpers/rest'
|
||||
import { arrayCollectionSlug } from './collections/Array'
|
||||
import { groupSlug } from './collections/Group'
|
||||
import { nestedToArrayAndBlockCollectionSlug } from './collections/NestedToArrayAndBlock'
|
||||
import { tabSlug } from './collections/Tab'
|
||||
import configPromise from './config'
|
||||
import { defaultLocale, hungarianLocale, localizedSortSlug } from './shared'
|
||||
import {
|
||||
@@ -52,7 +54,6 @@ describe('Localization', () => {
|
||||
|
||||
config = await configPromise
|
||||
|
||||
// @ts-expect-error Force typing
|
||||
post1 = await payload.create({
|
||||
collection,
|
||||
data: {
|
||||
@@ -60,7 +61,6 @@ describe('Localization', () => {
|
||||
},
|
||||
})
|
||||
|
||||
// @ts-expect-error Force typing
|
||||
postWithLocalizedData = await payload.create({
|
||||
collection,
|
||||
data: {
|
||||
@@ -1060,6 +1060,747 @@ describe('Localization', () => {
|
||||
expect(rowSpanish.textNotLocalized).toEqual('test')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Localized group and tabs', () => {
|
||||
it('should properly create/update/read localized group field', async () => {
|
||||
const result = await payload.create({
|
||||
collection: groupSlug,
|
||||
data: {
|
||||
groupLocalized: {
|
||||
title: 'hello en',
|
||||
},
|
||||
},
|
||||
locale: englishLocale,
|
||||
})
|
||||
|
||||
expect(result.groupLocalized?.title).toBe('hello en')
|
||||
|
||||
await payload.update({
|
||||
collection: groupSlug,
|
||||
locale: spanishLocale,
|
||||
id: result.id,
|
||||
data: {
|
||||
groupLocalized: {
|
||||
title: 'hello es',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const docEn = await payload.findByID({
|
||||
collection: groupSlug,
|
||||
locale: englishLocale,
|
||||
id: result.id,
|
||||
})
|
||||
const docEs = await payload.findByID({
|
||||
collection: groupSlug,
|
||||
locale: spanishLocale,
|
||||
id: result.id,
|
||||
})
|
||||
|
||||
expect(docEn.groupLocalized.title).toBe('hello en')
|
||||
expect(docEs.groupLocalized.title).toBe('hello es')
|
||||
})
|
||||
|
||||
it('should properly create/update/read localized field inside of group', async () => {
|
||||
const result = await payload.create({
|
||||
collection: groupSlug,
|
||||
locale: englishLocale,
|
||||
data: {
|
||||
group: {
|
||||
title: 'hello en',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.group.title).toBe('hello en')
|
||||
|
||||
await payload.update({
|
||||
collection: groupSlug,
|
||||
locale: spanishLocale,
|
||||
id: result.id,
|
||||
data: {
|
||||
group: {
|
||||
title: 'hello es',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const docEn = await payload.findByID({
|
||||
collection: groupSlug,
|
||||
locale: englishLocale,
|
||||
id: result.id,
|
||||
})
|
||||
const docEs = await payload.findByID({
|
||||
collection: groupSlug,
|
||||
locale: spanishLocale,
|
||||
id: result.id,
|
||||
})
|
||||
|
||||
expect(docEn.group.title).toBe('hello en')
|
||||
expect(docEs.group.title).toBe('hello es')
|
||||
})
|
||||
|
||||
it('should properly create/update/read deep localized field inside of group', async () => {
|
||||
const result = await payload.create({
|
||||
collection: groupSlug,
|
||||
locale: englishLocale,
|
||||
data: {
|
||||
deep: {
|
||||
blocks: [
|
||||
{
|
||||
blockType: 'first',
|
||||
title: 'hello en',
|
||||
},
|
||||
],
|
||||
array: [{ title: 'hello en' }],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.deep.array[0].title).toBe('hello en')
|
||||
|
||||
await payload.update({
|
||||
collection: groupSlug,
|
||||
locale: spanishLocale,
|
||||
id: result.id,
|
||||
data: {
|
||||
deep: {
|
||||
blocks: [
|
||||
{
|
||||
blockType: 'first',
|
||||
title: 'hello es',
|
||||
id: result.deep.blocks[0].id,
|
||||
},
|
||||
],
|
||||
array: [
|
||||
{
|
||||
id: result.deep.array[0].id,
|
||||
title: 'hello es',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const docEn = await payload.findByID({
|
||||
collection: groupSlug,
|
||||
locale: englishLocale,
|
||||
id: result.id,
|
||||
})
|
||||
const docEs = await payload.findByID({
|
||||
collection: groupSlug,
|
||||
locale: spanishLocale,
|
||||
id: result.id,
|
||||
})
|
||||
|
||||
expect(docEn.deep.array[0].title).toBe('hello en')
|
||||
expect(docEn.deep.blocks[0].title).toBe('hello en')
|
||||
expect(docEs.deep.array[0].title).toBe('hello es')
|
||||
expect(docEs.deep.blocks[0].title).toBe('hello es')
|
||||
})
|
||||
|
||||
it('should create/updated/read localized group with row field', async () => {
|
||||
const doc = await payload.create({
|
||||
collection: 'groups',
|
||||
data: {
|
||||
groupLocalizedRow: {
|
||||
text: 'hello world',
|
||||
},
|
||||
},
|
||||
locale: 'en',
|
||||
})
|
||||
|
||||
expect(doc.groupLocalizedRow.text).toBe('hello world')
|
||||
|
||||
const docES = await payload.update({
|
||||
collection: 'groups',
|
||||
data: {
|
||||
groupLocalizedRow: {
|
||||
text: 'hola world or something',
|
||||
},
|
||||
},
|
||||
locale: 'es',
|
||||
id: doc.id,
|
||||
})
|
||||
|
||||
expect(docES.groupLocalizedRow.text).toBe('hola world or something')
|
||||
|
||||
// check if docES didnt break EN
|
||||
const docEN = await payload.findByID({ collection: 'groups', id: doc.id, locale: 'en' })
|
||||
expect(docEN.groupLocalizedRow.text).toBe('hello world')
|
||||
|
||||
const all = await payload.findByID({ collection: 'groups', id: doc.id, locale: 'all' })
|
||||
|
||||
expect(all.groupLocalizedRow.en.text).toBe('hello world')
|
||||
expect(all.groupLocalizedRow.es.text).toBe('hola world or something')
|
||||
})
|
||||
|
||||
it('should properly create/update/read localized tab field', async () => {
|
||||
const result = await payload.create({
|
||||
collection: tabSlug,
|
||||
locale: englishLocale,
|
||||
data: {
|
||||
tabLocalized: {
|
||||
title: 'hello en',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.tabLocalized?.title).toBe('hello en')
|
||||
|
||||
await payload.update({
|
||||
collection: tabSlug,
|
||||
locale: spanishLocale,
|
||||
id: result.id,
|
||||
data: {
|
||||
tabLocalized: {
|
||||
title: 'hello es',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const docEn = await payload.findByID({
|
||||
collection: tabSlug,
|
||||
locale: englishLocale,
|
||||
id: result.id,
|
||||
})
|
||||
const docEs = await payload.findByID({
|
||||
collection: tabSlug,
|
||||
locale: spanishLocale,
|
||||
id: result.id,
|
||||
})
|
||||
|
||||
expect(docEn.tabLocalized.title).toBe('hello en')
|
||||
expect(docEs.tabLocalized.title).toBe('hello es')
|
||||
})
|
||||
|
||||
it('should properly create/update/read localized field inside of tab', async () => {
|
||||
const result = await payload.create({
|
||||
collection: tabSlug,
|
||||
locale: englishLocale,
|
||||
data: {
|
||||
tab: {
|
||||
title: 'hello en',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.tab.title).toBe('hello en')
|
||||
|
||||
await payload.update({
|
||||
collection: tabSlug,
|
||||
locale: spanishLocale,
|
||||
id: result.id,
|
||||
data: {
|
||||
tab: {
|
||||
title: 'hello es',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const docEn = await payload.findByID({
|
||||
collection: tabSlug,
|
||||
locale: englishLocale,
|
||||
id: result.id,
|
||||
})
|
||||
const docEs = await payload.findByID({
|
||||
collection: tabSlug,
|
||||
locale: spanishLocale,
|
||||
id: result.id,
|
||||
})
|
||||
|
||||
expect(docEn.tab.title).toBe('hello en')
|
||||
expect(docEs.tab.title).toBe('hello es')
|
||||
})
|
||||
|
||||
it('should properly create/update/read deep localized field inside of tab', async () => {
|
||||
const result = await payload.create({
|
||||
collection: tabSlug,
|
||||
locale: englishLocale,
|
||||
data: {
|
||||
deep: {
|
||||
blocks: [
|
||||
{
|
||||
blockType: 'first',
|
||||
title: 'hello en',
|
||||
},
|
||||
],
|
||||
array: [{ title: 'hello en' }],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.deep.array[0].title).toBe('hello en')
|
||||
|
||||
await payload.update({
|
||||
collection: tabSlug,
|
||||
locale: spanishLocale,
|
||||
id: result.id,
|
||||
data: {
|
||||
deep: {
|
||||
blocks: [
|
||||
{
|
||||
blockType: 'first',
|
||||
title: 'hello es',
|
||||
id: result.deep.blocks[0].id,
|
||||
},
|
||||
],
|
||||
array: [
|
||||
{
|
||||
id: result.deep.array[0].id,
|
||||
title: 'hello es',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const docEn = await payload.findByID({
|
||||
collection: tabSlug,
|
||||
locale: englishLocale,
|
||||
id: result.id,
|
||||
})
|
||||
const docEs = await payload.findByID({
|
||||
collection: tabSlug,
|
||||
locale: spanishLocale,
|
||||
id: result.id,
|
||||
})
|
||||
|
||||
expect(docEn.deep.array[0].title).toBe('hello en')
|
||||
expect(docEn.deep.blocks[0].title).toBe('hello en')
|
||||
expect(docEs.deep.array[0].title).toBe('hello es')
|
||||
expect(docEs.deep.blocks[0].title).toBe('hello es')
|
||||
})
|
||||
})
|
||||
|
||||
describe('nested blocks', () => {
|
||||
let id
|
||||
it('should allow creating nested blocks per locale', async () => {
|
||||
const doc = await payload.create({
|
||||
collection: 'blocks-fields',
|
||||
data: {
|
||||
content: [
|
||||
{
|
||||
blockType: 'blockInsideBlock',
|
||||
content: [
|
||||
{
|
||||
blockType: 'textBlock',
|
||||
text: 'hello',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
id = doc.id
|
||||
|
||||
await payload.update({
|
||||
collection: 'blocks-fields',
|
||||
id,
|
||||
locale: 'es',
|
||||
data: {
|
||||
content: [
|
||||
{
|
||||
blockType: 'blockInsideBlock',
|
||||
content: [
|
||||
{
|
||||
blockType: 'textBlock',
|
||||
text: 'hola',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const retrieved = await payload.findByID({
|
||||
collection: 'blocks-fields',
|
||||
id,
|
||||
locale: 'all',
|
||||
})
|
||||
|
||||
expect(retrieved.content.en[0].content).toHaveLength(1)
|
||||
expect(retrieved.content.es[0].content).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('nested arrays', () => {
|
||||
it('should not duplicate block rows for blocks within localized array fields', async () => {
|
||||
const randomDoc = (
|
||||
await payload.find({
|
||||
collection: 'localized-posts',
|
||||
depth: 0,
|
||||
})
|
||||
).docs[0]
|
||||
|
||||
const randomDoc2 = (
|
||||
await payload.find({
|
||||
collection: 'localized-posts',
|
||||
depth: 0,
|
||||
})
|
||||
).docs[1]
|
||||
|
||||
const blocksWithinArrayEN = [
|
||||
{
|
||||
blockName: '1',
|
||||
blockType: 'someBlock',
|
||||
relationWithinBlock: randomDoc.id,
|
||||
},
|
||||
{
|
||||
blockName: '2',
|
||||
blockType: 'someBlock',
|
||||
relationWithinBlock: randomDoc.id,
|
||||
},
|
||||
{
|
||||
blockName: '3',
|
||||
blockType: 'someBlock',
|
||||
relationWithinBlock: randomDoc.id,
|
||||
},
|
||||
]
|
||||
|
||||
const blocksWithinArrayES = [
|
||||
{
|
||||
blockName: '1',
|
||||
blockType: 'someBlock',
|
||||
relationWithinBlock: randomDoc2.id,
|
||||
},
|
||||
{
|
||||
blockName: '2',
|
||||
blockType: 'someBlock',
|
||||
relationWithinBlock: randomDoc2.id,
|
||||
},
|
||||
{
|
||||
blockName: '3',
|
||||
blockType: 'someBlock',
|
||||
relationWithinBlock: randomDoc2.id,
|
||||
},
|
||||
]
|
||||
|
||||
const createdEnDoc = await payload.create({
|
||||
collection: 'nested-arrays',
|
||||
locale: 'en',
|
||||
depth: 0,
|
||||
data: {
|
||||
arrayWithBlocks: [
|
||||
{
|
||||
blocksWithinArray: blocksWithinArrayEN as any,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const updatedEsDoc = await payload.update({
|
||||
collection: 'nested-arrays',
|
||||
id: createdEnDoc.id,
|
||||
depth: 0,
|
||||
locale: 'es',
|
||||
data: {
|
||||
arrayWithBlocks: [
|
||||
{
|
||||
blocksWithinArray: blocksWithinArrayES as any,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const esArrayBlocks = updatedEsDoc.arrayWithBlocks[0].blocksWithinArray
|
||||
// recursively remove any id field within esArrayRow
|
||||
const removeId = (obj) => {
|
||||
if (obj instanceof Object) {
|
||||
delete obj.id
|
||||
Object.values(obj).forEach(removeId)
|
||||
}
|
||||
}
|
||||
removeId(esArrayBlocks)
|
||||
removeId(createdEnDoc.arrayWithBlocks[0].blocksWithinArray)
|
||||
|
||||
expect(esArrayBlocks).toEqual(blocksWithinArrayES)
|
||||
expect(createdEnDoc.arrayWithBlocks[0].blocksWithinArray).toEqual(blocksWithinArrayEN)
|
||||
|
||||
// pull enDoc again and make sure the update of esDoc did not mess with the data of enDoc
|
||||
const enDoc2 = await payload.findByID({
|
||||
id: createdEnDoc.id,
|
||||
collection: 'nested-arrays',
|
||||
locale: 'en',
|
||||
depth: 0,
|
||||
})
|
||||
removeId(enDoc2.arrayWithBlocks[0].blocksWithinArray)
|
||||
expect(enDoc2.arrayWithBlocks[0].blocksWithinArray).toEqual(blocksWithinArrayEN)
|
||||
})
|
||||
|
||||
it('should update localized relation within unLocalized array', async () => {
|
||||
const randomTextDoc = (
|
||||
await payload.find({
|
||||
collection: 'localized-posts',
|
||||
depth: 0,
|
||||
})
|
||||
).docs[0]
|
||||
const randomTextDoc2 = (
|
||||
await payload.find({
|
||||
collection: 'localized-posts',
|
||||
depth: 0,
|
||||
})
|
||||
).docs[1]
|
||||
|
||||
const createdEnDoc = await payload.create({
|
||||
collection: 'nested-arrays',
|
||||
locale: 'en',
|
||||
depth: 0,
|
||||
data: {
|
||||
arrayWithLocalizedRelation: [
|
||||
{
|
||||
localizedRelation: randomTextDoc.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const updatedEsDoc = await payload.update({
|
||||
collection: 'nested-arrays',
|
||||
id: createdEnDoc.id,
|
||||
depth: 0,
|
||||
locale: 'es',
|
||||
data: {
|
||||
arrayWithLocalizedRelation: [
|
||||
{
|
||||
id: createdEnDoc.arrayWithLocalizedRelation[0].id,
|
||||
localizedRelation: randomTextDoc2.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(updatedEsDoc.arrayWithLocalizedRelation).toHaveLength(1)
|
||||
expect(updatedEsDoc.arrayWithLocalizedRelation[0].localizedRelation).toBe(randomTextDoc2.id)
|
||||
|
||||
expect(createdEnDoc.arrayWithLocalizedRelation).toHaveLength(1)
|
||||
expect(createdEnDoc.arrayWithLocalizedRelation[0].localizedRelation).toBe(randomTextDoc.id)
|
||||
|
||||
// pull enDoc again and make sure the update of esDoc did not mess with the data of enDoc
|
||||
const enDoc2 = await payload.findByID({
|
||||
id: createdEnDoc.id,
|
||||
collection: 'nested-arrays',
|
||||
locale: 'en',
|
||||
depth: 0,
|
||||
})
|
||||
expect(enDoc2.arrayWithLocalizedRelation).toHaveLength(1)
|
||||
expect(enDoc2.arrayWithLocalizedRelation[0].localizedRelation).toBe(randomTextDoc.id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('nested fields', () => {
|
||||
it('should allow for fields which could contain new tables within localized arrays to be stored', async () => {
|
||||
const randomDoc = (
|
||||
await payload.find({
|
||||
collection: 'localized-posts',
|
||||
depth: 0,
|
||||
})
|
||||
).docs[0]
|
||||
const randomDoc2 = (
|
||||
await payload.find({
|
||||
collection: 'localized-posts',
|
||||
depth: 0,
|
||||
})
|
||||
).docs[1]
|
||||
|
||||
const newDoc = await payload.create({
|
||||
collection: 'nested-field-tables',
|
||||
data: {
|
||||
array: [
|
||||
{
|
||||
relation: {
|
||||
value: randomDoc.id,
|
||||
relationTo: 'localized-posts',
|
||||
},
|
||||
hasManyRelation: [randomDoc.id, randomDoc2.id],
|
||||
hasManyPolyRelation: [
|
||||
{
|
||||
relationTo: 'localized-posts',
|
||||
value: randomDoc.id,
|
||||
},
|
||||
{
|
||||
relationTo: 'localized-posts',
|
||||
value: randomDoc2.id,
|
||||
},
|
||||
],
|
||||
number: [1, 2],
|
||||
text: ['hello', 'goodbye'],
|
||||
select: ['one'],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
await payload.update({
|
||||
collection: 'nested-field-tables',
|
||||
id: newDoc.id,
|
||||
locale: 'es',
|
||||
data: {
|
||||
array: [
|
||||
{
|
||||
relation: {
|
||||
value: randomDoc2.id,
|
||||
relationTo: 'localized-posts',
|
||||
},
|
||||
hasManyRelation: [randomDoc2.id, randomDoc.id],
|
||||
hasManyPolyRelation: [
|
||||
{
|
||||
relationTo: 'localized-posts',
|
||||
value: randomDoc2.id,
|
||||
},
|
||||
{
|
||||
relationTo: 'localized-posts',
|
||||
value: randomDoc.id,
|
||||
},
|
||||
],
|
||||
select: ['two', 'three'],
|
||||
text: ['hola', 'adios'],
|
||||
number: [3, 4],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const retrieved = await payload.findByID({
|
||||
collection: 'nested-field-tables',
|
||||
id: newDoc.id,
|
||||
depth: 0,
|
||||
locale: 'all',
|
||||
})
|
||||
|
||||
expect(retrieved.array.en[0].relation.value).toStrictEqual(randomDoc.id)
|
||||
expect(retrieved.array.es[0].relation.value).toStrictEqual(randomDoc2.id)
|
||||
|
||||
expect(retrieved.array.en[0].hasManyRelation).toEqual([randomDoc.id, randomDoc2.id])
|
||||
expect(retrieved.array.es[0].hasManyRelation).toEqual([randomDoc2.id, randomDoc.id])
|
||||
|
||||
expect(retrieved.array.en[0].hasManyPolyRelation).toEqual([
|
||||
{ value: randomDoc.id, relationTo: 'localized-posts' },
|
||||
{ value: randomDoc2.id, relationTo: 'localized-posts' },
|
||||
])
|
||||
expect(retrieved.array.es[0].hasManyPolyRelation).toEqual([
|
||||
{ value: randomDoc2.id, relationTo: 'localized-posts' },
|
||||
{ value: randomDoc.id, relationTo: 'localized-posts' },
|
||||
])
|
||||
|
||||
expect(retrieved.array.en[0].number).toEqual([1, 2])
|
||||
expect(retrieved.array.es[0].number).toEqual([3, 4])
|
||||
|
||||
expect(retrieved.array.en[0].select).toEqual(['one'])
|
||||
expect(retrieved.array.es[0].select).toEqual(['two', 'three'])
|
||||
|
||||
expect(retrieved.array.en[0].text).toEqual(['hello', 'goodbye'])
|
||||
expect(retrieved.array.es[0].text).toEqual(['hola', 'adios'])
|
||||
})
|
||||
|
||||
it('should duplicate with localized blocks', async () => {
|
||||
// This test covers a few things:
|
||||
// - make sure localized arrays / blocks work inside of localized groups / tabs
|
||||
// - this is covered with myTab.group.nestedArray2
|
||||
|
||||
const englishText = 'english'
|
||||
const spanishText = 'spanish'
|
||||
const doc = await payload.create({
|
||||
collection: withRequiredLocalizedFields,
|
||||
data: {
|
||||
layout: [
|
||||
{
|
||||
blockType: 'text',
|
||||
text: englishText,
|
||||
nestedArray: [
|
||||
{
|
||||
text: 'hello',
|
||||
},
|
||||
{
|
||||
text: 'goodbye',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
myTab: {
|
||||
text: 'hello',
|
||||
group: {
|
||||
nestedText: 'hello',
|
||||
nestedArray2: [
|
||||
{
|
||||
nestedText: 'hello',
|
||||
},
|
||||
{
|
||||
nestedText: 'goodbye',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
title: 'hello',
|
||||
},
|
||||
locale: defaultLocale,
|
||||
})
|
||||
|
||||
await payload.update({
|
||||
id: doc.id,
|
||||
collection: withRequiredLocalizedFields,
|
||||
data: {
|
||||
layout: [
|
||||
{
|
||||
blockType: 'text',
|
||||
text: spanishText,
|
||||
nestedArray: [
|
||||
{
|
||||
text: 'hola',
|
||||
},
|
||||
{
|
||||
text: 'adios',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
title: 'hello',
|
||||
myTab: {
|
||||
text: 'hola',
|
||||
group: {
|
||||
nestedText: 'hola',
|
||||
nestedArray2: [
|
||||
{
|
||||
nestedText: 'hola',
|
||||
},
|
||||
{
|
||||
nestedText: 'adios',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
locale: spanishLocale,
|
||||
})
|
||||
|
||||
const result = await payload.findByID({
|
||||
id: doc.id,
|
||||
collection: withRequiredLocalizedFields,
|
||||
locale: defaultLocale,
|
||||
})
|
||||
|
||||
const allLocales = await payload.findByID({
|
||||
id: result.id,
|
||||
collection: withRequiredLocalizedFields,
|
||||
locale: 'all',
|
||||
})
|
||||
|
||||
// check fields
|
||||
expect(result.layout[0].text).toStrictEqual(englishText)
|
||||
|
||||
expect(allLocales.layout.en[0].text).toStrictEqual(englishText)
|
||||
expect(allLocales.layout.es[0].text).toStrictEqual(spanishText)
|
||||
|
||||
expect(allLocales.myTab.group.en.nestedText).toStrictEqual('hello')
|
||||
expect(allLocales.myTab.group.en.nestedArray2[0].nestedText).toStrictEqual('hello')
|
||||
expect(allLocales.myTab.group.en.nestedArray2[1].nestedText).toStrictEqual('goodbye')
|
||||
|
||||
expect(allLocales.myTab.group.es.nestedText).toStrictEqual('hola')
|
||||
expect(allLocales.myTab.group.es.nestedArray2[0].nestedText).toStrictEqual('hola')
|
||||
expect(allLocales.myTab.group.es.nestedArray2[1].nestedText).toStrictEqual('adios')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
async function createLocalizedPost(data: {
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
|
||||
export interface Config {
|
||||
collections: {
|
||||
'blocks-fields': BlocksField
|
||||
'nested-arrays': NestedArray
|
||||
'nested-field-tables': NestedFieldTable
|
||||
users: User
|
||||
'localized-posts': LocalizedPost
|
||||
'array-fields': ArrayField
|
||||
@@ -15,6 +18,12 @@ export interface Config {
|
||||
'with-localized-relationship': WithLocalizedRelationship
|
||||
'relationship-localized': RelationshipLocalized
|
||||
dummy: Dummy
|
||||
'nested-to-array-and-block': NestedToArrayAndBlock
|
||||
groups: Group
|
||||
tabs: Tab
|
||||
'localized-sort': LocalizedSort
|
||||
'blocks-same-name': BlocksSameName
|
||||
'restricted-by-locale': RestrictedByLocale
|
||||
'payload-preferences': PayloadPreference
|
||||
'payload-migrations': PayloadMigration
|
||||
}
|
||||
@@ -22,9 +31,139 @@ export interface Config {
|
||||
'global-array': GlobalArray
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "blocks-fields".
|
||||
*/
|
||||
export interface BlocksField {
|
||||
id: string
|
||||
content?:
|
||||
| {
|
||||
content?:
|
||||
| {
|
||||
text?: string | null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'textBlock'
|
||||
}[]
|
||||
| null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'blockInsideBlock'
|
||||
}[]
|
||||
| null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "nested-arrays".
|
||||
*/
|
||||
export interface NestedArray {
|
||||
id: string
|
||||
arrayWithBlocks?:
|
||||
| {
|
||||
blocksWithinArray?:
|
||||
| {
|
||||
relationWithinBlock?: (string | null) | LocalizedPost
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'someBlock'
|
||||
}[]
|
||||
| null
|
||||
id?: string | null
|
||||
}[]
|
||||
| null
|
||||
arrayWithLocalizedRelation?:
|
||||
| {
|
||||
localizedRelation?: (string | null) | LocalizedPost
|
||||
id?: string | null
|
||||
}[]
|
||||
| null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "localized-posts".
|
||||
*/
|
||||
export interface LocalizedPost {
|
||||
id: string
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
localizedDescription?: string | null
|
||||
localizedCheckbox?: boolean | null
|
||||
children?: (string | LocalizedPost)[] | null
|
||||
group?: {
|
||||
children?: string | null
|
||||
}
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "nested-field-tables".
|
||||
*/
|
||||
export interface NestedFieldTable {
|
||||
id: string
|
||||
array?:
|
||||
| {
|
||||
relation?: {
|
||||
relationTo: 'localized-posts'
|
||||
value: string | LocalizedPost
|
||||
} | null
|
||||
hasManyRelation?: (string | LocalizedPost)[] | null
|
||||
hasManyPolyRelation?:
|
||||
| {
|
||||
relationTo: 'localized-posts'
|
||||
value: string | LocalizedPost
|
||||
}[]
|
||||
| null
|
||||
select?: ('one' | 'two' | 'three')[] | null
|
||||
number?: number[] | null
|
||||
text?: string[] | null
|
||||
id?: string | null
|
||||
}[]
|
||||
| null
|
||||
blocks?:
|
||||
| {
|
||||
nestedBlocks?:
|
||||
| {
|
||||
relation?: {
|
||||
relationTo: 'localized-posts'
|
||||
value: string | LocalizedPost
|
||||
} | null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'content'
|
||||
}[]
|
||||
| null
|
||||
array?:
|
||||
| {
|
||||
relation?: {
|
||||
relationTo: 'localized-posts'
|
||||
value: string | LocalizedPost
|
||||
} | null
|
||||
id?: string | null
|
||||
}[]
|
||||
| null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'block'
|
||||
}[]
|
||||
| null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string
|
||||
relation?: (string | null) | LocalizedPost
|
||||
assignedLocales?: ('en' | 'es' | 'pt' | 'ar')[] | null
|
||||
roles?: ('admin' | 'editor') | null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
email: string
|
||||
@@ -36,14 +175,10 @@ export interface User {
|
||||
lockUntil?: string | null
|
||||
password: string | null
|
||||
}
|
||||
export interface LocalizedPost {
|
||||
id: string
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
localizedCheckbox?: boolean | null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "array-fields".
|
||||
*/
|
||||
export interface ArrayField {
|
||||
id: string
|
||||
items?:
|
||||
@@ -55,12 +190,22 @@ export interface ArrayField {
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "localized-required".
|
||||
*/
|
||||
export interface LocalizedRequired {
|
||||
id: string
|
||||
title: string
|
||||
layout: (
|
||||
| {
|
||||
text?: string | null
|
||||
nestedArray?:
|
||||
| {
|
||||
text?: string | null
|
||||
id?: string | null
|
||||
}[]
|
||||
| null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'text'
|
||||
@@ -72,9 +217,25 @@ export interface LocalizedRequired {
|
||||
blockType: 'number'
|
||||
}
|
||||
)[]
|
||||
myTab: {
|
||||
text?: string | null
|
||||
group?: {
|
||||
nestedArray2?:
|
||||
| {
|
||||
nestedText?: string | null
|
||||
id?: string | null
|
||||
}[]
|
||||
| null
|
||||
nestedText?: string | null
|
||||
}
|
||||
}
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "with-localized-relationship".
|
||||
*/
|
||||
export interface WithLocalizedRelationship {
|
||||
id: string
|
||||
localizedRelationship?: (string | null) | LocalizedPost
|
||||
@@ -103,12 +264,20 @@ export interface WithLocalizedRelationship {
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "dummy".
|
||||
*/
|
||||
export interface Dummy {
|
||||
id: string
|
||||
name?: string | null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "relationship-localized".
|
||||
*/
|
||||
export interface RelationshipLocalized {
|
||||
id: string
|
||||
relationship?: (string | null) | LocalizedPost
|
||||
@@ -143,6 +312,144 @@ export interface RelationshipLocalized {
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "nested-to-array-and-block".
|
||||
*/
|
||||
export interface NestedToArrayAndBlock {
|
||||
id: string
|
||||
blocks?:
|
||||
| {
|
||||
array?:
|
||||
| {
|
||||
text?: string | null
|
||||
textNotLocalized?: string | null
|
||||
id?: string | null
|
||||
}[]
|
||||
| null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'block'
|
||||
}[]
|
||||
| null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "groups".
|
||||
*/
|
||||
export interface Group {
|
||||
id: string
|
||||
groupLocalizedRow?: {
|
||||
text?: string | null
|
||||
}
|
||||
groupLocalized?: {
|
||||
title?: string | null
|
||||
}
|
||||
group?: {
|
||||
title?: string | null
|
||||
}
|
||||
deep?: {
|
||||
array?:
|
||||
| {
|
||||
title?: string | null
|
||||
id?: string | null
|
||||
}[]
|
||||
| null
|
||||
blocks?:
|
||||
| {
|
||||
title?: string | null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'first'
|
||||
}[]
|
||||
| null
|
||||
}
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "tabs".
|
||||
*/
|
||||
export interface Tab {
|
||||
id: string
|
||||
tabLocalized: {
|
||||
title?: string | null
|
||||
}
|
||||
tab: {
|
||||
title?: string | null
|
||||
}
|
||||
deep: {
|
||||
array?:
|
||||
| {
|
||||
title?: string | null
|
||||
id?: string | null
|
||||
}[]
|
||||
| null
|
||||
blocks?:
|
||||
| {
|
||||
title?: string | null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'first'
|
||||
}[]
|
||||
| null
|
||||
}
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "localized-sort".
|
||||
*/
|
||||
export interface LocalizedSort {
|
||||
id: string
|
||||
title?: string | null
|
||||
date?: string | null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "blocks-same-name".
|
||||
*/
|
||||
export interface BlocksSameName {
|
||||
id: string
|
||||
blocks?:
|
||||
| (
|
||||
| {
|
||||
title?: string | null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'block_first'
|
||||
}
|
||||
| {
|
||||
title?: string | null
|
||||
id?: string | null
|
||||
blockName?: string | null
|
||||
blockType: 'block_second'
|
||||
}
|
||||
)[]
|
||||
| null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "restricted-by-locale".
|
||||
*/
|
||||
export interface RestrictedByLocale {
|
||||
id: string
|
||||
title?: string | null
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string
|
||||
user: {
|
||||
@@ -162,6 +469,10 @@ export interface PayloadPreference {
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string
|
||||
name?: string | null
|
||||
@@ -169,6 +480,10 @@ export interface PayloadMigration {
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "global-array".
|
||||
*/
|
||||
export interface GlobalArray {
|
||||
id: string
|
||||
array?:
|
||||
|
||||
@@ -123,6 +123,18 @@ describe('uploads', () => {
|
||||
await saveDocAndAssert(page)
|
||||
})
|
||||
|
||||
test('should properly create IOS file upload', async () => {
|
||||
await page.goto(mediaURL.create)
|
||||
|
||||
await page.setInputFiles('input[type="file"]', path.resolve(__dirname, './ios-image.jpeg'))
|
||||
|
||||
const filename = page.locator('.file-field__filename')
|
||||
|
||||
await expect(filename).toHaveValue('ios-image.jpeg')
|
||||
|
||||
await saveDocAndAssert(page)
|
||||
})
|
||||
|
||||
test('should create animated file upload', async () => {
|
||||
await page.goto(animatedTypeMediaURL.create)
|
||||
|
||||
@@ -559,6 +571,34 @@ describe('uploads', () => {
|
||||
// without focal point update this generated size was equal to 1736
|
||||
expect(redDoc.sizes.focalTest.filesize).toEqual(1598)
|
||||
})
|
||||
|
||||
test('should resize image after crop if resizeOptions defined', async () => {
|
||||
await page.goto(animatedTypeMediaURL.create)
|
||||
await page.waitForURL(animatedTypeMediaURL.create)
|
||||
|
||||
const fileChooserPromise = page.waitForEvent('filechooser')
|
||||
await page.getByText('Select a file').click()
|
||||
const fileChooser = await fileChooserPromise
|
||||
await wait(1000)
|
||||
await fileChooser.setFiles(path.join(__dirname, 'horizontal-squares.jpg'))
|
||||
|
||||
await page.locator('.file-field__edit').click()
|
||||
|
||||
// set crop
|
||||
await page.locator('.edit-upload__input input[name="Width (px)"]').fill('400')
|
||||
await page.locator('.edit-upload__input input[name="Height (px)"]').fill('800')
|
||||
// set focal point
|
||||
await page.locator('.edit-upload__input input[name="X %"]').fill('75') // init left focal point
|
||||
await page.locator('.edit-upload__input input[name="Y %"]').fill('50') // init top focal point
|
||||
|
||||
await page.locator('button:has-text("Apply Changes")').click()
|
||||
await page.waitForSelector('button#action-save')
|
||||
await page.locator('button#action-save').click()
|
||||
await wait(1000) // Wait for the save
|
||||
|
||||
const resizeOptionMedia = page.locator('.file-meta .file-meta__size-type')
|
||||
await expect(resizeOptionMedia).toContainText('200x200')
|
||||
})
|
||||
})
|
||||
|
||||
test('should see upload previews in relation list if allowed in config', async () => {
|
||||
|
||||
BIN
test/uploads/ios-image.jpeg
Normal file
BIN
test/uploads/ios-image.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Reference in New Issue
Block a user