Compare commits
44 Commits
fix/postgr
...
perf/field
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6124e26468 | ||
|
|
d4899b84cc | ||
|
|
6fb2beb983 | ||
|
|
4166621966 | ||
|
|
e395a0aa66 | ||
|
|
cead312d4b | ||
|
|
5342d303ea | ||
|
|
766236f38e | ||
|
|
219fd01717 | ||
|
|
1f6efe9a46 | ||
|
|
88769c8244 | ||
|
|
bd6ee317c1 | ||
|
|
561708720d | ||
|
|
58fc2f9a74 | ||
|
|
5fce501589 | ||
|
|
3e7db302ee | ||
|
|
7498d09f1c | ||
|
|
3edfd7cc6d | ||
|
|
77bb7e3638 | ||
|
|
8ebadd4190 | ||
|
|
e258cd73ef | ||
|
|
d63c8baea5 | ||
|
|
93d79b9c62 | ||
|
|
9779cf7f7d | ||
|
|
b7b2b390fc | ||
|
|
7130834152 | ||
|
|
1d5d96d2c3 | ||
|
|
faa7794cc7 | ||
|
|
98283ca18c | ||
|
|
e93d0baf89 | ||
|
|
cd455741e5 | ||
|
|
735d699804 | ||
|
|
d9c0c43154 | ||
|
|
a9cc747038 | ||
|
|
fd67d461ac | ||
|
|
8219c046de | ||
|
|
021932cc8b | ||
|
|
edeb381fb4 | ||
|
|
c43891b2ba | ||
|
|
3701de5056 | ||
|
|
09f15ff874 | ||
|
|
72662257a8 | ||
|
|
18693775e4 | ||
|
|
b3cac753d6 |
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
@@ -6,7 +6,7 @@ inputs:
|
||||
node-version:
|
||||
description: Node.js version
|
||||
required: true
|
||||
default: 22.6.0
|
||||
default: 23.11.0
|
||||
pnpm-version:
|
||||
description: Pnpm version
|
||||
required: true
|
||||
|
||||
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -16,7 +16,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.6.0
|
||||
NODE_VERSION: 23.11.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
2
.github/workflows/post-release-templates.yml
vendored
2
.github/workflows/post-release-templates.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.6.0
|
||||
NODE_VERSION: 23.11.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
2
.github/workflows/post-release.yml
vendored
2
.github/workflows/post-release.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
default: ''
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.6.0
|
||||
NODE_VERSION: 23.11.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
2
.github/workflows/publish-prerelease.yml
vendored
2
.github/workflows/publish-prerelease.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.6.0
|
||||
NODE_VERSION: 23.11.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="true" type="JavaScriptTestRunnerJest">
|
||||
<node-interpreter value="project" />
|
||||
<node-options value="--no-deprecation" />
|
||||
<envs />
|
||||
<scope-kind value="ALL" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -1 +1 @@
|
||||
v22.6.0
|
||||
v23.11.0
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
pnpm 9.7.1
|
||||
nodejs 22.6.0
|
||||
nodejs 23.11.0
|
||||
|
||||
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@@ -63,6 +63,13 @@
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts query-presets",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Query Presets",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts login-with-username",
|
||||
"cwd": "${workspaceFolder}",
|
||||
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -24,5 +24,8 @@
|
||||
"runtimeArgs": ["--no-deprecation"]
|
||||
},
|
||||
// Essentially disables bun test buttons
|
||||
"bun.test.filePattern": "bun.test.ts"
|
||||
"bun.test.filePattern": "bun.test.ts",
|
||||
"playwright.env": {
|
||||
"NODE_OPTIONS": "--no-deprecation --no-experimental-strip-types"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,9 @@ export const MyGroupField: Field = {
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`name`** | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`fields`** \* | Array of field types to nest within this Group. |
|
||||
| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. |
|
||||
| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. Required when name is undefined, defaults to name converted to words. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
@@ -86,7 +86,7 @@ export const ExampleCollection: CollectionConfig = {
|
||||
slug: 'example-collection',
|
||||
fields: [
|
||||
{
|
||||
name: 'pageMeta', // required
|
||||
name: 'pageMeta',
|
||||
type: 'group', // required
|
||||
interfaceName: 'Meta', // optional
|
||||
fields: [
|
||||
@@ -110,3 +110,38 @@ export const ExampleCollection: CollectionConfig = {
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Presentational group fields
|
||||
|
||||
You can also use the Group field to create a presentational group of fields. This is useful when you want to group fields together visually without affecting the data structure.
|
||||
The label will be required when a `name` is not provided.
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const ExampleCollection: CollectionConfig = {
|
||||
slug: 'example-collection',
|
||||
fields: [
|
||||
{
|
||||
label: 'Page meta',
|
||||
type: 'group', // required
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
minLength: 20,
|
||||
maxLength: 100,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
minLength: 40,
|
||||
maxLength: 160,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
@@ -63,19 +63,50 @@ const config = buildConfig({
|
||||
export default config
|
||||
```
|
||||
|
||||
Now in your Next.js app, include the `?encodeSourceMaps=true` parameter in any of your API requests. For performance reasons, this should only be done when in draft mode or on preview deployments.
|
||||
## Enabling Content Source Maps
|
||||
|
||||
Now in your Next.js app, you need to add the `encodeSourceMaps` query parameter to your API requests. This will tell Payload to include the Content Source Maps in the API response.
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:** For performance reasons, this should only be done when in draft mode
|
||||
or on preview deployments.
|
||||
</Banner>
|
||||
|
||||
#### REST API
|
||||
|
||||
If you're using the REST API, include the `?encodeSourceMaps=true` search parameter.
|
||||
|
||||
```ts
|
||||
if (isDraftMode || process.env.VERCEL_ENV === 'preview') {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_PAYLOAD_CMS_URL}/api/pages?where[slug][equals]=${slug}&encodeSourceMaps=true`,
|
||||
`${process.env.NEXT_PUBLIC_PAYLOAD_CMS_URL}/api/pages?encodeSourceMaps=true&where[slug][equals]=${slug}`,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### Local API
|
||||
|
||||
If you're using the Local API, include the `encodeSourceMaps` via the `context` property.
|
||||
|
||||
```ts
|
||||
if (isDraftMode || process.env.VERCEL_ENV === 'preview') {
|
||||
const res = await payload.find({
|
||||
collection: 'pages',
|
||||
where: {
|
||||
slug: {
|
||||
equals: slug,
|
||||
},
|
||||
},
|
||||
context: {
|
||||
encodeSourceMaps: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
And that's it! You are now ready to enter Edit Mode and begin visually editing your content.
|
||||
|
||||
#### Edit Mode
|
||||
## Edit Mode
|
||||
|
||||
To see Content Link on your site, you first need to visit any preview deployment on Vercel and login using the Vercel Toolbar. When Content Source Maps are detected on the page, a pencil icon will appear in the toolbar. Clicking this icon will enable Edit Mode, highlighting all editable fields on the page in blue.
|
||||
|
||||
@@ -94,7 +125,9 @@ const { cleaned, encoded } = vercelStegaSplit(text)
|
||||
|
||||
### Blocks and array fields
|
||||
|
||||
All `blocks` and `array` fields by definition do not have plain text strings to encode. For this reason, they are given an additional `_encodedSourceMap` property, which you can use to enable Content Link on entire _sections_ of your site. You can then specify the editing container by adding the `data-vercel-edit-target` HTML attribute to any top-level element of your block.
|
||||
All `blocks` and `array` fields by definition do not have plain text strings to encode. For this reason, they are automatically given an additional `_encodedSourceMap` property, which you can use to enable Content Link on entire _sections_ of your site.
|
||||
|
||||
You can then specify the editing container by adding the `data-vercel-edit-target` HTML attribute to any top-level element of your block.
|
||||
|
||||
```ts
|
||||
<div data-vercel-edit-target>
|
||||
|
||||
@@ -309,7 +309,3 @@ import {
|
||||
...
|
||||
} from '@payloadcms/plugin-stripe/types';
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) contains an official [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommerce) which demonstrates exactly how to configure this plugin in Payload and implement it on your front-end. You can also check out [How to Build An E-Commerce Site With Next.js](https://payloadcms.com/blog/how-to-build-an-e-commerce-site-with-nextjs) post for a bit more context around this template.
|
||||
|
||||
@@ -74,6 +74,7 @@ export const rootEslintConfig = [
|
||||
'no-console': 'off',
|
||||
'perfectionist/sort-object-types': 'off',
|
||||
'perfectionist/sort-objects': 'off',
|
||||
'payload/no-relative-monorepo-imports': 'off',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { BeforeSync, DocToSync } from '@payloadcms/plugin-search/types'
|
||||
|
||||
export const beforeSyncWithSearch: BeforeSync = async ({ originalDoc, searchDoc, payload }) => {
|
||||
export const beforeSyncWithSearch: BeforeSync = async ({ req, originalDoc, searchDoc }) => {
|
||||
const {
|
||||
doc: { relationTo: collection },
|
||||
} = searchDoc
|
||||
|
||||
const { slug, id, categories, title, meta, excerpt } = originalDoc
|
||||
const { slug, id, categories, title, meta } = originalDoc
|
||||
|
||||
const modifiedDoc: DocToSync = {
|
||||
...searchDoc,
|
||||
@@ -20,24 +20,40 @@ export const beforeSyncWithSearch: BeforeSync = async ({ originalDoc, searchDoc,
|
||||
}
|
||||
|
||||
if (categories && Array.isArray(categories) && categories.length > 0) {
|
||||
// get full categories and keep a flattened copy of their most important properties
|
||||
try {
|
||||
const mappedCategories = categories.map((category) => {
|
||||
const { id, title } = category
|
||||
const populatedCategories: { id: string | number; title: string }[] = []
|
||||
for (const category of categories) {
|
||||
if (!category) {
|
||||
continue
|
||||
}
|
||||
|
||||
return {
|
||||
relationTo: 'categories',
|
||||
id,
|
||||
title,
|
||||
}
|
||||
if (typeof category === 'object') {
|
||||
populatedCategories.push(category)
|
||||
continue
|
||||
}
|
||||
|
||||
const doc = await req.payload.findByID({
|
||||
collection: 'categories',
|
||||
id: category,
|
||||
disableErrors: true,
|
||||
depth: 0,
|
||||
select: { title: true },
|
||||
req,
|
||||
})
|
||||
|
||||
modifiedDoc.categories = mappedCategories
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Failed. Category not found when syncing collection '${collection}' with id: '${id}' to search.`,
|
||||
)
|
||||
if (doc !== null) {
|
||||
populatedCategories.push(doc)
|
||||
} else {
|
||||
console.error(
|
||||
`Failed. Category not found when syncing collection '${collection}' with id: '${id}' to search.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
modifiedDoc.categories = populatedCategories.map((each) => ({
|
||||
relationTo: 'categories',
|
||||
categoryID: String(each.id),
|
||||
title: each.title,
|
||||
}))
|
||||
}
|
||||
|
||||
return modifiedDoc
|
||||
|
||||
@@ -52,7 +52,7 @@ export const searchFields: Field[] = [
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
name: 'categoryID',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.37.0",
|
||||
"version": "3.38.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/admin-bar",
|
||||
"version": "3.37.0",
|
||||
"version": "3.38.0",
|
||||
"description": "An admin bar for React apps using Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.37.0",
|
||||
"version": "3.38.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -22,7 +22,9 @@ const updateEnvExampleVariables = (
|
||||
|
||||
const [key] = line.split('=')
|
||||
|
||||
if (!key) {return}
|
||||
if (!key) {
|
||||
return
|
||||
}
|
||||
|
||||
if (key === 'DATABASE_URI' || key === 'POSTGRES_URL' || key === 'MONGODB_URI') {
|
||||
const dbChoice = databaseType ? dbChoiceRecord[databaseType] : null
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.37.0",
|
||||
"version": "3.38.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -372,36 +372,61 @@ const group: FieldSchemaGenerator<GroupField> = (
|
||||
buildSchemaOptions,
|
||||
parentIsLocalized,
|
||||
): void => {
|
||||
const formattedBaseSchema = formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized })
|
||||
if (fieldAffectsData(field)) {
|
||||
const formattedBaseSchema = formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized })
|
||||
|
||||
// carry indexSortableFields through to versions if drafts enabled
|
||||
const indexSortableFields =
|
||||
buildSchemaOptions.indexSortableFields &&
|
||||
field.name === 'version' &&
|
||||
buildSchemaOptions.draftsEnabled
|
||||
// carry indexSortableFields through to versions if drafts enabled
|
||||
const indexSortableFields =
|
||||
buildSchemaOptions.indexSortableFields &&
|
||||
field.name === 'version' &&
|
||||
buildSchemaOptions.draftsEnabled
|
||||
|
||||
const baseSchema: SchemaTypeOptions<any> = {
|
||||
...formattedBaseSchema,
|
||||
type: buildSchema({
|
||||
buildSchemaOptions: {
|
||||
disableUnique: buildSchemaOptions.disableUnique,
|
||||
draftsEnabled: buildSchemaOptions.draftsEnabled,
|
||||
indexSortableFields,
|
||||
options: {
|
||||
_id: false,
|
||||
id: false,
|
||||
minimize: false,
|
||||
const baseSchema: SchemaTypeOptions<any> = {
|
||||
...formattedBaseSchema,
|
||||
type: buildSchema({
|
||||
buildSchemaOptions: {
|
||||
disableUnique: buildSchemaOptions.disableUnique,
|
||||
draftsEnabled: buildSchemaOptions.draftsEnabled,
|
||||
indexSortableFields,
|
||||
options: {
|
||||
_id: false,
|
||||
id: false,
|
||||
minimize: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
configFields: field.fields,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
payload,
|
||||
}),
|
||||
}
|
||||
configFields: field.fields,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
payload,
|
||||
}),
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, payload.config.localization, parentIsLocalized),
|
||||
})
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(
|
||||
field,
|
||||
baseSchema,
|
||||
payload.config.localization,
|
||||
parentIsLocalized,
|
||||
),
|
||||
})
|
||||
} else {
|
||||
field.fields.forEach((subField) => {
|
||||
if (fieldIsVirtual(subField)) {
|
||||
return
|
||||
}
|
||||
|
||||
const addFieldSchema = getSchemaGenerator(subField.type)
|
||||
|
||||
if (addFieldSchema) {
|
||||
addFieldSchema(
|
||||
subField,
|
||||
schema,
|
||||
payload,
|
||||
buildSchemaOptions,
|
||||
(parentIsLocalized || field.localized) ?? false,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const json: FieldSchemaGenerator<JSONField> = (
|
||||
|
||||
@@ -57,12 +57,8 @@ const relationshipSort = ({
|
||||
return false
|
||||
}
|
||||
|
||||
for (const [i, segment] of segments.entries()) {
|
||||
if (versions && i === 0 && segment === 'version') {
|
||||
segments.shift()
|
||||
continue
|
||||
}
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const segment = segments[i]
|
||||
const field = currentFields.find((each) => each.name === segment)
|
||||
|
||||
if (!field) {
|
||||
@@ -71,6 +67,10 @@ const relationshipSort = ({
|
||||
|
||||
if ('fields' in field) {
|
||||
currentFields = field.flattenedFields
|
||||
if (field.name === 'version' && versions && i === 0) {
|
||||
segments.shift()
|
||||
i--
|
||||
}
|
||||
} else if (
|
||||
(field.type === 'relationship' || field.type === 'upload') &&
|
||||
i !== segments.length - 1
|
||||
@@ -106,7 +106,7 @@ const relationshipSort = ({
|
||||
as: `__${path}`,
|
||||
foreignField: '_id',
|
||||
from: foreignCollection.Model.collection.name,
|
||||
localField: relationshipPath,
|
||||
localField: versions ? `version.${relationshipPath}` : relationshipPath,
|
||||
pipeline: [
|
||||
{
|
||||
$project: {
|
||||
|
||||
@@ -105,6 +105,7 @@ export const sanitizeQueryValue = ({
|
||||
| undefined => {
|
||||
let formattedValue = val
|
||||
let formattedOperator = operator
|
||||
|
||||
if (['array', 'blocks', 'group', 'tab'].includes(field.type) && path.includes('.')) {
|
||||
const segments = path.split('.')
|
||||
segments.shift()
|
||||
|
||||
@@ -151,6 +151,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
query: versionQuery,
|
||||
session: paginationOptions.options?.session ?? undefined,
|
||||
sort: paginationOptions.sort as object,
|
||||
sortAggregation,
|
||||
useEstimatedCount: paginationOptions.useEstimatedCount,
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -128,7 +128,6 @@ const traverseFields = ({
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const blocksSelect = select[field.name] as SelectType
|
||||
|
||||
|
||||
@@ -425,6 +425,7 @@ export const transform = ({
|
||||
for (const locale of config.localization.localeCodes) {
|
||||
sanitizeDate({
|
||||
field,
|
||||
locale,
|
||||
ref: fieldRef,
|
||||
value: fieldRef[locale],
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.37.0",
|
||||
"version": "3.38.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.37.0",
|
||||
"version": "3.38.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.37.0",
|
||||
"version": "3.38.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.37.0",
|
||||
"version": "3.38.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -53,6 +53,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"console-table-printer": "2.12.1",
|
||||
"dequal": "2.0.3",
|
||||
"drizzle-orm": "0.36.1",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { DrizzleAdapter } from '../types.js'
|
||||
import buildQuery from '../queries/buildQuery.js'
|
||||
import { selectDistinct } from '../queries/selectDistinct.js'
|
||||
import { transform } from '../transform/read/index.js'
|
||||
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
|
||||
import { getTransaction } from '../utilities/getTransaction.js'
|
||||
import { buildFindManyArgs } from './buildFindManyArgs.js'
|
||||
|
||||
@@ -75,6 +76,26 @@ export const findMany = async function find({
|
||||
tableName,
|
||||
versions,
|
||||
})
|
||||
|
||||
if (orderBy) {
|
||||
for (const key in selectFields) {
|
||||
const column = selectFields[key]
|
||||
if (column.primary) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
!orderBy.some(
|
||||
(col) =>
|
||||
col.column.name === column.name &&
|
||||
getNameFromDrizzleTable(col.column.table) === getNameFromDrizzleTable(column.table),
|
||||
)
|
||||
) {
|
||||
delete selectFields[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selectDistinctResult = await selectDistinct({
|
||||
adapter,
|
||||
db,
|
||||
|
||||
@@ -22,9 +22,14 @@ import type { Result } from './buildFindManyArgs.js'
|
||||
import buildQuery from '../queries/buildQuery.js'
|
||||
import { getTableAlias } from '../queries/getTableAlias.js'
|
||||
import { operatorMap } from '../queries/operatorMap.js'
|
||||
import { getArrayRelationName } from '../utilities/getArrayRelationName.js'
|
||||
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
|
||||
import { jsonAggBuildObject } from '../utilities/json.js'
|
||||
import { rawConstraint } from '../utilities/rawConstraint.js'
|
||||
import {
|
||||
InternalBlockTableNameIndex,
|
||||
resolveBlockTableName,
|
||||
} from '../utilities/validateExistingBlockIsIdentical.js'
|
||||
|
||||
const flattenAllWherePaths = (where: Where, paths: string[]) => {
|
||||
for (const k in where) {
|
||||
@@ -196,7 +201,12 @@ export const traverseFields = ({
|
||||
}
|
||||
}
|
||||
|
||||
const relationName = field.dbName ? `_${arrayTableName}` : `${path}${field.name}`
|
||||
const relationName = getArrayRelationName({
|
||||
field,
|
||||
path: `${path}${field.name}`,
|
||||
tableName: arrayTableName,
|
||||
})
|
||||
|
||||
currentArgs.with[relationName] = withArray
|
||||
|
||||
traverseFields({
|
||||
@@ -244,7 +254,7 @@ export const traverseFields = ({
|
||||
|
||||
;(field.blockReferences ?? field.blocks).forEach((_block) => {
|
||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||
const blockKey = `_blocks_${block.slug}`
|
||||
const blockKey = `_blocks_${block.slug}${!block[InternalBlockTableNameIndex] ? '' : `_${block[InternalBlockTableNameIndex]}`}`
|
||||
|
||||
let blockSelect: boolean | SelectType | undefined
|
||||
|
||||
@@ -284,8 +294,9 @@ export const traverseFields = ({
|
||||
with: {},
|
||||
}
|
||||
|
||||
const tableName = adapter.tableNameMap.get(
|
||||
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
const tableName = resolveBlockTableName(
|
||||
block,
|
||||
adapter.tableNameMap.get(`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`),
|
||||
)
|
||||
|
||||
if (typeof blockSelect === 'object') {
|
||||
|
||||
@@ -1,49 +1,126 @@
|
||||
export type Groups =
|
||||
| 'addColumn'
|
||||
| 'addConstraint'
|
||||
| 'alterType'
|
||||
| 'createIndex'
|
||||
| 'createTable'
|
||||
| 'createType'
|
||||
| 'disableRowSecurity'
|
||||
| 'dropColumn'
|
||||
| 'dropConstraint'
|
||||
| 'dropIndex'
|
||||
| 'dropTable'
|
||||
| 'dropType'
|
||||
| 'notNull'
|
||||
| 'renameColumn'
|
||||
| 'setDefault'
|
||||
|
||||
/**
|
||||
* Convert an "ADD COLUMN" statement to an "ALTER COLUMN" statement
|
||||
* example: ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer NOT NULL;
|
||||
* to: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;
|
||||
* @param sql
|
||||
* Convert an "ADD COLUMN" statement to an "ALTER COLUMN" statement.
|
||||
* Works with or without a schema name.
|
||||
*
|
||||
* Examples:
|
||||
* 'ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer NOT NULL;'
|
||||
* => 'ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;'
|
||||
*
|
||||
* 'ALTER TABLE "public"."pages_blocks_my_block" ADD COLUMN "person_id" integer NOT NULL;'
|
||||
* => 'ALTER TABLE "public"."pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;'
|
||||
*/
|
||||
function convertAddColumnToAlterColumn(sql) {
|
||||
// Regular expression to match the ADD COLUMN statement with its constraints
|
||||
const regex = /ALTER TABLE ("[^"]+")\.(".*?") ADD COLUMN ("[^"]+") [\w\s]+ NOT NULL;/
|
||||
const regex = /ALTER TABLE ((?:"[^"]+"\.)?"[^"]+") ADD COLUMN ("[^"]+") [^;]*?NOT NULL;/i
|
||||
|
||||
// Replace the matched part with "ALTER COLUMN ... SET NOT NULL;"
|
||||
return sql.replace(regex, 'ALTER TABLE $1.$2 ALTER COLUMN $3 SET NOT NULL;')
|
||||
return sql.replace(regex, 'ALTER TABLE $1 ALTER COLUMN $2 SET NOT NULL;')
|
||||
}
|
||||
|
||||
export const groupUpSQLStatements = (list: string[]): Record<Groups, string[]> => {
|
||||
const groups = {
|
||||
/**
|
||||
* example: ALTER TABLE "posts" ADD COLUMN "category_id" integer
|
||||
*/
|
||||
addColumn: 'ADD COLUMN',
|
||||
// example: ALTER TABLE "posts" ADD COLUMN "category_id" integer
|
||||
|
||||
/**
|
||||
* example:
|
||||
* DO $$ BEGIN
|
||||
* ALTER TABLE "pages_blocks_my_block" ADD CONSTRAINT "pages_blocks_my_block_person_id_users_id_fk" FOREIGN KEY ("person_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
* EXCEPTION
|
||||
* WHEN duplicate_object THEN null;
|
||||
* END $$;
|
||||
*/
|
||||
addConstraint: 'ADD CONSTRAINT',
|
||||
//example:
|
||||
// DO $$ BEGIN
|
||||
// ALTER TABLE "pages_blocks_my_block" ADD CONSTRAINT "pages_blocks_my_block_person_id_users_id_fk" FOREIGN KEY ("person_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
// EXCEPTION
|
||||
// WHEN duplicate_object THEN null;
|
||||
// END $$;
|
||||
|
||||
/**
|
||||
* example: CREATE TABLE IF NOT EXISTS "payload_locked_documents" (
|
||||
* "id" serial PRIMARY KEY NOT NULL,
|
||||
* "global_slug" varchar,
|
||||
* "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
* "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
* );
|
||||
*/
|
||||
createTable: 'CREATE TABLE',
|
||||
|
||||
/**
|
||||
* example: ALTER TABLE "_posts_v_rels" DROP COLUMN IF EXISTS "posts_id";
|
||||
*/
|
||||
dropColumn: 'DROP COLUMN',
|
||||
// example: ALTER TABLE "_posts_v_rels" DROP COLUMN IF EXISTS "posts_id";
|
||||
|
||||
/**
|
||||
* example: ALTER TABLE "_posts_v_rels" DROP CONSTRAINT "_posts_v_rels_posts_fk";
|
||||
*/
|
||||
dropConstraint: 'DROP CONSTRAINT',
|
||||
// example: ALTER TABLE "_posts_v_rels" DROP CONSTRAINT "_posts_v_rels_posts_fk";
|
||||
|
||||
/**
|
||||
* example: DROP TABLE "pages_rels";
|
||||
*/
|
||||
dropTable: 'DROP TABLE',
|
||||
// example: DROP TABLE "pages_rels";
|
||||
|
||||
/**
|
||||
* example: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;
|
||||
*/
|
||||
notNull: 'NOT NULL',
|
||||
// example: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;
|
||||
|
||||
/**
|
||||
* example: CREATE TYPE "public"."enum__pages_v_published_locale" AS ENUM('en', 'es');
|
||||
*/
|
||||
createType: 'CREATE TYPE',
|
||||
|
||||
/**
|
||||
* example: ALTER TYPE "public"."enum_pages_blocks_cta" ADD VALUE 'copy';
|
||||
*/
|
||||
alterType: 'ALTER TYPE',
|
||||
|
||||
/**
|
||||
* example: ALTER TABLE "categories_rels" DISABLE ROW LEVEL SECURITY;
|
||||
*/
|
||||
disableRowSecurity: 'DISABLE ROW LEVEL SECURITY;',
|
||||
|
||||
/**
|
||||
* example: DROP INDEX IF EXISTS "pages_title_idx";
|
||||
*/
|
||||
dropIndex: 'DROP INDEX IF EXISTS',
|
||||
|
||||
/**
|
||||
* example: ALTER TABLE "pages" ALTER COLUMN "_status" SET DEFAULT 'draft';
|
||||
*/
|
||||
setDefault: 'SET DEFAULT',
|
||||
|
||||
/**
|
||||
* example: CREATE INDEX IF NOT EXISTS "payload_locked_documents_global_slug_idx" ON "payload_locked_documents" USING btree ("global_slug");
|
||||
*/
|
||||
createIndex: 'INDEX IF NOT EXISTS',
|
||||
|
||||
/**
|
||||
* example: DROP TYPE "public"."enum__pages_v_published_locale";
|
||||
*/
|
||||
dropType: 'DROP TYPE',
|
||||
|
||||
/**
|
||||
* columns were renamed from camelCase to snake_case
|
||||
* example: ALTER TABLE "forms" RENAME COLUMN "confirmationType" TO "confirmation_type";
|
||||
*/
|
||||
renameColumn: 'RENAME COLUMN',
|
||||
}
|
||||
|
||||
const result = Object.keys(groups).reduce((result, group: Groups) => {
|
||||
@@ -51,7 +128,17 @@ export const groupUpSQLStatements = (list: string[]): Record<Groups, string[]> =
|
||||
return result
|
||||
}, {}) as Record<Groups, string[]>
|
||||
|
||||
// push multi-line changes to a single grouping
|
||||
let isCreateTable = false
|
||||
|
||||
for (const line of list) {
|
||||
if (isCreateTable) {
|
||||
result.createTable.push(line)
|
||||
if (line.includes(');')) {
|
||||
isCreateTable = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
Object.entries(groups).some(([key, value]) => {
|
||||
if (line.endsWith('NOT NULL;')) {
|
||||
// split up the ADD COLUMN and ALTER COLUMN NOT NULL statements
|
||||
@@ -64,7 +151,11 @@ export const groupUpSQLStatements = (list: string[]): Record<Groups, string[]> =
|
||||
return true
|
||||
}
|
||||
if (line.includes(value)) {
|
||||
result[key].push(line)
|
||||
let statement = line
|
||||
if (key === 'dropConstraint') {
|
||||
statement = line.replace('" DROP CONSTRAINT "', '" DROP CONSTRAINT IF EXISTS "')
|
||||
}
|
||||
result[key].push(statement)
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,6 +20,17 @@ type Args = {
|
||||
req?: Partial<PayloadRequest>
|
||||
}
|
||||
|
||||
const runStatementGroup = async ({ adapter, db, debug, statements }) => {
|
||||
const addColumnsStatement = statements.join('\n')
|
||||
|
||||
if (debug) {
|
||||
adapter.payload.logger.info(debug)
|
||||
adapter.payload.logger.info(addColumnsStatement)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(addColumnsStatement))
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves upload and relationship columns from the join table and into the tables while moving data
|
||||
* This is done in the following order:
|
||||
@@ -40,16 +51,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
|
||||
|
||||
// get the drizzle migrateUpSQL from drizzle using the last schema
|
||||
const { generateDrizzleJson, generateMigration, upSnapshot } = adapter.requireDrizzleKit()
|
||||
|
||||
const toSnapshot: Record<string, unknown> = {}
|
||||
|
||||
for (const key of Object.keys(adapter.schema).filter(
|
||||
(key) => !key.startsWith('payload_locked_documents'),
|
||||
)) {
|
||||
toSnapshot[key] = adapter.schema[key]
|
||||
}
|
||||
|
||||
const drizzleJsonAfter = generateDrizzleJson(toSnapshot) as DrizzleSnapshotJSON
|
||||
const drizzleJsonAfter = generateDrizzleJson(adapter.schema) as DrizzleSnapshotJSON
|
||||
|
||||
// Get the previous migration snapshot
|
||||
const previousSnapshot = fs
|
||||
@@ -81,18 +83,62 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
|
||||
|
||||
const sqlUpStatements = groupUpSQLStatements(generatedSQL)
|
||||
|
||||
const addColumnsStatement = sqlUpStatements.addColumn.join('\n')
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('CREATING NEW RELATIONSHIP COLUMNS')
|
||||
payload.logger.info(addColumnsStatement)
|
||||
}
|
||||
|
||||
const db = await getTransaction(adapter, req)
|
||||
|
||||
await db.execute(sql.raw(addColumnsStatement))
|
||||
await runStatementGroup({
|
||||
adapter,
|
||||
db,
|
||||
debug: debug ? 'CREATING TYPES' : null,
|
||||
statements: sqlUpStatements.createType,
|
||||
})
|
||||
|
||||
await runStatementGroup({
|
||||
adapter,
|
||||
db,
|
||||
debug: debug ? 'ALTERING TYPES' : null,
|
||||
statements: sqlUpStatements.alterType,
|
||||
})
|
||||
|
||||
await runStatementGroup({
|
||||
adapter,
|
||||
db,
|
||||
debug: debug ? 'CREATING TABLES' : null,
|
||||
statements: sqlUpStatements.createTable,
|
||||
})
|
||||
|
||||
await runStatementGroup({
|
||||
adapter,
|
||||
db,
|
||||
debug: debug ? 'RENAMING COLUMNS' : null,
|
||||
statements: sqlUpStatements.renameColumn,
|
||||
})
|
||||
|
||||
await runStatementGroup({
|
||||
adapter,
|
||||
db,
|
||||
debug: debug ? 'CREATING NEW RELATIONSHIP COLUMNS' : null,
|
||||
statements: sqlUpStatements.addColumn,
|
||||
})
|
||||
|
||||
// SET DEFAULTS
|
||||
await runStatementGroup({
|
||||
adapter,
|
||||
db,
|
||||
debug: debug ? 'SETTING DEFAULTS' : null,
|
||||
statements: sqlUpStatements.setDefault,
|
||||
})
|
||||
|
||||
await runStatementGroup({
|
||||
adapter,
|
||||
db,
|
||||
debug: debug ? 'CREATING INDEXES' : null,
|
||||
statements: sqlUpStatements.createIndex,
|
||||
})
|
||||
|
||||
for (const collection of payload.config.collections) {
|
||||
if (collection.slug === 'payload-locked-documents') {
|
||||
continue
|
||||
}
|
||||
const tableName = adapter.tableNameMap.get(toSnakeCase(collection.slug))
|
||||
const pathsToQuery: PathsToQuery = new Set()
|
||||
|
||||
@@ -238,52 +284,58 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
|
||||
}
|
||||
|
||||
// ADD CONSTRAINT
|
||||
const addConstraintsStatement = sqlUpStatements.addConstraint.join('\n')
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('ADDING CONSTRAINTS')
|
||||
payload.logger.info(addConstraintsStatement)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(addConstraintsStatement))
|
||||
await runStatementGroup({
|
||||
adapter,
|
||||
db,
|
||||
debug: debug ? 'ADDING CONSTRAINTS' : null,
|
||||
statements: sqlUpStatements.addConstraint,
|
||||
})
|
||||
|
||||
// NOT NULL
|
||||
const notNullStatements = sqlUpStatements.notNull.join('\n')
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('NOT NULL CONSTRAINTS')
|
||||
payload.logger.info(notNullStatements)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(notNullStatements))
|
||||
await runStatementGroup({
|
||||
adapter,
|
||||
db,
|
||||
debug: debug ? 'NOT NULL CONSTRAINTS' : null,
|
||||
statements: sqlUpStatements.notNull,
|
||||
})
|
||||
|
||||
// DROP TABLE
|
||||
const dropTablesStatement = sqlUpStatements.dropTable.join('\n')
|
||||
await runStatementGroup({
|
||||
adapter,
|
||||
db,
|
||||
debug: debug ? 'DROPPING TABLES' : null,
|
||||
statements: sqlUpStatements.dropTable,
|
||||
})
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('DROPPING TABLES')
|
||||
payload.logger.info(dropTablesStatement)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(dropTablesStatement))
|
||||
// DROP INDEX
|
||||
await runStatementGroup({
|
||||
adapter,
|
||||
db,
|
||||
debug: debug ? 'DROPPING INDEXES' : null,
|
||||
statements: sqlUpStatements.dropIndex,
|
||||
})
|
||||
|
||||
// DROP CONSTRAINT
|
||||
const dropConstraintsStatement = sqlUpStatements.dropConstraint.join('\n')
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('DROPPING CONSTRAINTS')
|
||||
payload.logger.info(dropConstraintsStatement)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(dropConstraintsStatement))
|
||||
await runStatementGroup({
|
||||
adapter,
|
||||
db,
|
||||
debug: debug ? 'DROPPING CONSTRAINTS' : null,
|
||||
statements: sqlUpStatements.dropConstraint,
|
||||
})
|
||||
|
||||
// DROP COLUMN
|
||||
const dropColumnsStatement = sqlUpStatements.dropColumn.join('\n')
|
||||
await runStatementGroup({
|
||||
adapter,
|
||||
db,
|
||||
debug: debug ? 'DROPPING COLUMNS' : null,
|
||||
statements: sqlUpStatements.dropColumn,
|
||||
})
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('DROPPING COLUMNS')
|
||||
payload.logger.info(dropColumnsStatement)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(dropColumnsStatement))
|
||||
// DROP TYPES
|
||||
await runStatementGroup({
|
||||
adapter,
|
||||
db,
|
||||
debug: debug ? 'DROPPING TYPES' : null,
|
||||
statements: sqlUpStatements.dropType,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export const migrateRelationships = async ({
|
||||
${where} ORDER BY parent_id LIMIT 500 OFFSET ${offset * 500};
|
||||
`
|
||||
|
||||
paginationResult = await adapter.drizzle.execute(sql.raw(`${paginationStatement}`))
|
||||
paginationResult = await db.execute(sql.raw(`${paginationStatement}`))
|
||||
|
||||
if (paginationResult.rows.length === 0) {
|
||||
return
|
||||
@@ -72,7 +72,7 @@ export const migrateRelationships = async ({
|
||||
payload.logger.info(statement)
|
||||
}
|
||||
|
||||
const result = await adapter.drizzle.execute(sql.raw(`${statement}`))
|
||||
const result = await db.execute(sql.raw(`${statement}`))
|
||||
|
||||
const docsToResave: DocsToResave = {}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Table } from 'drizzle-orm'
|
||||
import type { SQL, Table } from 'drizzle-orm'
|
||||
import type { FlattenedField, Sort } from 'payload'
|
||||
|
||||
import { asc, desc } from 'drizzle-orm'
|
||||
import { asc, desc, or } from 'drizzle-orm'
|
||||
|
||||
import type { DrizzleAdapter, GenericColumn } from '../types.js'
|
||||
import type { BuildQueryJoinAliases, BuildQueryResult } from './buildQuery.js'
|
||||
@@ -16,6 +16,7 @@ type Args = {
|
||||
joins: BuildQueryJoinAliases
|
||||
locale?: string
|
||||
parentIsLocalized: boolean
|
||||
rawSort?: SQL
|
||||
selectFields: Record<string, GenericColumn>
|
||||
sort?: Sort
|
||||
tableName: string
|
||||
@@ -31,6 +32,7 @@ export const buildOrderBy = ({
|
||||
joins,
|
||||
locale,
|
||||
parentIsLocalized,
|
||||
rawSort,
|
||||
selectFields,
|
||||
sort,
|
||||
tableName,
|
||||
@@ -74,12 +76,18 @@ export const buildOrderBy = ({
|
||||
value: sortProperty,
|
||||
})
|
||||
if (sortTable?.[sortTableColumnName]) {
|
||||
let order = sortDirection === 'asc' ? asc : desc
|
||||
|
||||
if (rawSort) {
|
||||
order = () => rawSort
|
||||
}
|
||||
|
||||
orderBy.push({
|
||||
column:
|
||||
aliasTable && tableName === getNameFromDrizzleTable(sortTable)
|
||||
? aliasTable[sortTableColumnName]
|
||||
: sortTable[sortTableColumnName],
|
||||
order: sortDirection === 'asc' ? asc : desc,
|
||||
order,
|
||||
})
|
||||
|
||||
selectFields[sortTableColumnName] = sortTable[sortTableColumnName]
|
||||
|
||||
@@ -79,6 +79,7 @@ const buildQuery = function buildQuery({
|
||||
joins,
|
||||
locale,
|
||||
parentIsLocalized,
|
||||
rawSort: context.rawSort,
|
||||
selectFields,
|
||||
sort: context.sort,
|
||||
tableName,
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { DrizzleAdapter, GenericColumn } from '../types.js'
|
||||
import type { BuildQueryJoinAliases } from './buildQuery.js'
|
||||
|
||||
import { isPolymorphicRelationship } from '../utilities/isPolymorphicRelationship.js'
|
||||
import { resolveBlockTableName } from '../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { addJoinTable } from './addJoinTable.js'
|
||||
import { getTableAlias } from './getTableAlias.js'
|
||||
|
||||
@@ -193,8 +194,9 @@ export const getTableColumnFromPath = ({
|
||||
(block) => typeof block !== 'string' && block.slug === blockType,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
newTableName = adapter.tableNameMap.get(
|
||||
`${tableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
newTableName = resolveBlockTableName(
|
||||
block,
|
||||
adapter.tableNameMap.get(`${tableName}_blocks_${toSnakeCase(block.slug)}`),
|
||||
)
|
||||
|
||||
const { newAliasTable } = getTableAlias({ adapter, tableName: newTableName })
|
||||
@@ -220,7 +222,11 @@ export const getTableColumnFromPath = ({
|
||||
const hasBlockField = (field.blockReferences ?? field.blocks).some((_block) => {
|
||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||
|
||||
newTableName = adapter.tableNameMap.get(`${tableName}_blocks_${toSnakeCase(block.slug)}`)
|
||||
newTableName = resolveBlockTableName(
|
||||
block,
|
||||
adapter.tableNameMap.get(`${tableName}_blocks_${toSnakeCase(block.slug)}`),
|
||||
)
|
||||
|
||||
constraintPath = `${constraintPath}${field.name}.%.`
|
||||
|
||||
let result: TableColumn
|
||||
|
||||
@@ -14,7 +14,7 @@ import { buildAndOrConditions } from './buildAndOrConditions.js'
|
||||
import { getTableColumnFromPath } from './getTableColumnFromPath.js'
|
||||
import { sanitizeQueryValue } from './sanitizeQueryValue.js'
|
||||
|
||||
export type QueryContext = { sort: Sort }
|
||||
export type QueryContext = { rawSort?: SQL; sort: Sort }
|
||||
|
||||
type Args = {
|
||||
adapter: DrizzleAdapter
|
||||
@@ -348,6 +348,7 @@ export function parseParams({
|
||||
}
|
||||
if (geoConstraints.length) {
|
||||
context.sort = relationOrPath
|
||||
context.rawSort = sql`${table[columnName]} <-> ST_SetSRID(ST_MakePoint(${lng}, ${lat}), 4326)`
|
||||
constraints.push(and(...geoConstraints))
|
||||
}
|
||||
break
|
||||
|
||||
@@ -32,6 +32,7 @@ type Args = {
|
||||
* ie. indexes, multiple columns, etc
|
||||
*/
|
||||
baseIndexes?: Record<string, RawIndex>
|
||||
blocksTableNameMap: Record<string, number>
|
||||
buildNumbers?: boolean
|
||||
buildRelationships?: boolean
|
||||
compoundIndexes?: SanitizedCompoundIndex[]
|
||||
@@ -70,6 +71,7 @@ export const buildTable = ({
|
||||
baseColumns = {},
|
||||
baseForeignKeys = {},
|
||||
baseIndexes = {},
|
||||
blocksTableNameMap,
|
||||
compoundIndexes,
|
||||
disableNotNull,
|
||||
disableRelsTableUnique = false,
|
||||
@@ -120,6 +122,7 @@ export const buildTable = ({
|
||||
hasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
blocksTableNameMap,
|
||||
columns,
|
||||
disableNotNull,
|
||||
disableRelsTableUnique,
|
||||
|
||||
@@ -56,6 +56,7 @@ export const buildRawSchema = ({
|
||||
|
||||
buildTable({
|
||||
adapter,
|
||||
blocksTableNameMap: {},
|
||||
compoundIndexes: collection.sanitizedIndexes,
|
||||
disableNotNull: !!collection?.versions?.drafts,
|
||||
disableUnique: false,
|
||||
@@ -75,6 +76,7 @@ export const buildRawSchema = ({
|
||||
|
||||
buildTable({
|
||||
adapter,
|
||||
blocksTableNameMap: {},
|
||||
compoundIndexes: buildVersionCompoundIndexes({ indexes: collection.sanitizedIndexes }),
|
||||
disableNotNull: !!collection.versions?.drafts,
|
||||
disableUnique: true,
|
||||
@@ -96,6 +98,7 @@ export const buildRawSchema = ({
|
||||
|
||||
buildTable({
|
||||
adapter,
|
||||
blocksTableNameMap: {},
|
||||
disableNotNull: !!global?.versions?.drafts,
|
||||
disableUnique: false,
|
||||
fields: global.flattenedFields,
|
||||
@@ -118,6 +121,7 @@ export const buildRawSchema = ({
|
||||
|
||||
buildTable({
|
||||
adapter,
|
||||
blocksTableNameMap: {},
|
||||
disableNotNull: !!global.versions?.drafts,
|
||||
disableUnique: true,
|
||||
fields: versionFields,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { CompoundIndex, FlattenedField } from 'payload'
|
||||
import type { FlattenedField } from 'payload'
|
||||
|
||||
import { InvalidConfiguration } from 'payload'
|
||||
import {
|
||||
array,
|
||||
fieldAffectsData,
|
||||
fieldIsVirtual,
|
||||
fieldShouldBeLocalized,
|
||||
@@ -23,14 +22,20 @@ import type {
|
||||
|
||||
import { createTableName } from '../createTableName.js'
|
||||
import { buildIndexName } from '../utilities/buildIndexName.js'
|
||||
import { getArrayRelationName } from '../utilities/getArrayRelationName.js'
|
||||
import { hasLocalesTable } from '../utilities/hasLocalesTable.js'
|
||||
import { validateExistingBlockIsIdentical } from '../utilities/validateExistingBlockIsIdentical.js'
|
||||
import {
|
||||
InternalBlockTableNameIndex,
|
||||
setInternalBlockIndex,
|
||||
validateExistingBlockIsIdentical,
|
||||
} from '../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { buildTable } from './build.js'
|
||||
import { idToUUID } from './idToUUID.js'
|
||||
import { withDefault } from './withDefault.js'
|
||||
|
||||
type Args = {
|
||||
adapter: DrizzleAdapter
|
||||
blocksTableNameMap: Record<string, number>
|
||||
columnPrefix?: string
|
||||
columns: Record<string, RawColumn>
|
||||
disableNotNull: boolean
|
||||
@@ -71,6 +76,7 @@ type Result = {
|
||||
|
||||
export const traverseFields = ({
|
||||
adapter,
|
||||
blocksTableNameMap,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull,
|
||||
@@ -249,6 +255,7 @@ export const traverseFields = ({
|
||||
baseColumns,
|
||||
baseForeignKeys,
|
||||
baseIndexes,
|
||||
blocksTableNameMap,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableRelsTableUnique: true,
|
||||
disableUnique,
|
||||
@@ -288,7 +295,11 @@ export const traverseFields = ({
|
||||
}
|
||||
}
|
||||
|
||||
const relationName = field.dbName ? `_${arrayTableName}` : fieldName
|
||||
const relationName = getArrayRelationName({
|
||||
field,
|
||||
path: fieldName,
|
||||
tableName: arrayTableName,
|
||||
})
|
||||
|
||||
relationsToBuild.set(relationName, {
|
||||
type: 'many',
|
||||
@@ -364,7 +375,7 @@ export const traverseFields = ({
|
||||
;(field.blockReferences ?? field.blocks).forEach((_block) => {
|
||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||
|
||||
const blockTableName = createTableName({
|
||||
let blockTableName = createTableName({
|
||||
adapter,
|
||||
config: block,
|
||||
parentTableName: rootTableName,
|
||||
@@ -372,6 +383,27 @@ export const traverseFields = ({
|
||||
throwValidationError,
|
||||
versionsCustomName: versions,
|
||||
})
|
||||
|
||||
if (typeof blocksTableNameMap[blockTableName] === 'undefined') {
|
||||
blocksTableNameMap[blockTableName] = 1
|
||||
} else if (
|
||||
!validateExistingBlockIsIdentical({
|
||||
block,
|
||||
localized: field.localized,
|
||||
rootTableName,
|
||||
table: adapter.rawTables[blockTableName],
|
||||
tableLocales: adapter.rawTables[`${blockTableName}${adapter.localesSuffix}`],
|
||||
})
|
||||
) {
|
||||
blocksTableNameMap[blockTableName]++
|
||||
setInternalBlockIndex(block, blocksTableNameMap[blockTableName])
|
||||
blockTableName = `${blockTableName}_${blocksTableNameMap[blockTableName]}`
|
||||
}
|
||||
let relationName = `_blocks_${block.slug}`
|
||||
if (typeof block[InternalBlockTableNameIndex] !== 'undefined') {
|
||||
relationName = `_blocks_${block.slug}_${block[InternalBlockTableNameIndex]}`
|
||||
}
|
||||
|
||||
if (!adapter.rawTables[blockTableName]) {
|
||||
const baseColumns: Record<string, RawColumn> = {
|
||||
_order: {
|
||||
@@ -451,6 +483,7 @@ export const traverseFields = ({
|
||||
baseColumns,
|
||||
baseForeignKeys,
|
||||
baseIndexes,
|
||||
blocksTableNameMap,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableRelsTableUnique: true,
|
||||
disableUnique,
|
||||
@@ -501,7 +534,7 @@ export const traverseFields = ({
|
||||
},
|
||||
],
|
||||
references: ['id'],
|
||||
relationName: `_blocks_${block.slug}`,
|
||||
relationName,
|
||||
to: rootTableName,
|
||||
},
|
||||
}
|
||||
@@ -549,18 +582,10 @@ export const traverseFields = ({
|
||||
})
|
||||
|
||||
adapter.rawRelations[blockTableName] = blockRelations
|
||||
} else if (process.env.NODE_ENV !== 'production' && !versions) {
|
||||
validateExistingBlockIsIdentical({
|
||||
block,
|
||||
localized: field.localized,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
rootTableName,
|
||||
table: adapter.rawTables[blockTableName],
|
||||
tableLocales: adapter.rawTables[`${blockTableName}${adapter.localesSuffix}`],
|
||||
})
|
||||
}
|
||||
|
||||
// blocks relationships are defined from the collection or globals table down to the block, bypassing any subBlocks
|
||||
rootRelationsToBuild.set(`_blocks_${block.slug}`, {
|
||||
rootRelationsToBuild.set(relationName, {
|
||||
type: 'many',
|
||||
// blocks are not localized on the parent table
|
||||
localized: false,
|
||||
@@ -624,6 +649,7 @@ export const traverseFields = ({
|
||||
hasManyTextField: groupHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
blocksTableNameMap,
|
||||
columnPrefix: `${columnName}_`,
|
||||
columns,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
@@ -840,6 +866,7 @@ export const traverseFields = ({
|
||||
baseColumns,
|
||||
baseForeignKeys,
|
||||
baseIndexes,
|
||||
blocksTableNameMap,
|
||||
disableNotNull,
|
||||
disableUnique,
|
||||
fields: [],
|
||||
|
||||
@@ -49,6 +49,7 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
|
||||
}
|
||||
|
||||
const blocks = createBlocksMap(data)
|
||||
|
||||
const deletions = []
|
||||
|
||||
const result = traverseFields<T>({
|
||||
|
||||
@@ -6,6 +6,8 @@ import toSnakeCase from 'to-snake-case'
|
||||
import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { BlocksMap } from '../../utilities/createBlocksMap.js'
|
||||
|
||||
import { getArrayRelationName } from '../../utilities/getArrayRelationName.js'
|
||||
import { resolveBlockTableName } from '../../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { transformHasManyNumber } from './hasManyNumber.js'
|
||||
import { transformHasManyText } from './hasManyText.js'
|
||||
import { transformRelationship } from './relationship.js'
|
||||
@@ -121,9 +123,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
`${currentTableName}_${tablePath}${toSnakeCase(field.name)}`,
|
||||
)
|
||||
|
||||
if (field.dbName) {
|
||||
fieldData = table[`_${arrayTableName}`]
|
||||
}
|
||||
fieldData = table[getArrayRelationName({ field, path: fieldName, tableName: arrayTableName })]
|
||||
|
||||
if (Array.isArray(fieldData)) {
|
||||
if (isLocalized) {
|
||||
@@ -249,8 +249,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
(block) => typeof block !== 'string' && block.slug === row.blockType,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
const tableName = adapter.tableNameMap.get(
|
||||
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
const tableName = resolveBlockTableName(
|
||||
block,
|
||||
adapter.tableNameMap.get(`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`),
|
||||
)
|
||||
|
||||
if (block) {
|
||||
@@ -328,8 +329,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
delete row._index
|
||||
}
|
||||
|
||||
const tableName = adapter.tableNameMap.get(
|
||||
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
const tableName = resolveBlockTableName(
|
||||
block,
|
||||
adapter.tableNameMap.get(
|
||||
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
),
|
||||
)
|
||||
|
||||
acc.push(
|
||||
|
||||
@@ -6,6 +6,7 @@ import toSnakeCase from 'to-snake-case'
|
||||
import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { BlockRowToInsert, RelationshipToDelete } from './types.js'
|
||||
|
||||
import { resolveBlockTableName } from '../../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args = {
|
||||
@@ -66,10 +67,6 @@ export const transformBlocks = ({
|
||||
}
|
||||
const blockType = toSnakeCase(blockRow.blockType)
|
||||
|
||||
if (!blocks[blockType]) {
|
||||
blocks[blockType] = []
|
||||
}
|
||||
|
||||
const newRow: BlockRowToInsert = {
|
||||
arrays: {},
|
||||
locales: {},
|
||||
@@ -86,7 +83,14 @@ export const transformBlocks = ({
|
||||
newRow.row._locale = withinArrayOrBlockLocale
|
||||
}
|
||||
|
||||
const blockTableName = adapter.tableNameMap.get(`${baseTableName}_blocks_${blockType}`)
|
||||
const blockTableName = resolveBlockTableName(
|
||||
matchedBlock,
|
||||
adapter.tableNameMap.get(`${baseTableName}_blocks_${blockType}`),
|
||||
)
|
||||
|
||||
if (!blocks[blockTableName]) {
|
||||
blocks[blockTableName] = []
|
||||
}
|
||||
|
||||
const hasUUID = adapter.tables[blockTableName]._uuid
|
||||
|
||||
@@ -124,6 +128,6 @@ export const transformBlocks = ({
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
blocks[blockType].push(newRow)
|
||||
blocks[blockTableName].push(newRow)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types.js'
|
||||
|
||||
import { isArrayOfRows } from '../../utilities/isArrayOfRows.js'
|
||||
import { resolveBlockTableName } from '../../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { transformArray } from './array.js'
|
||||
import { transformBlocks } from './blocks.js'
|
||||
import { transformNumbers } from './numbers.js'
|
||||
@@ -175,7 +176,17 @@ export const traverseFields = ({
|
||||
|
||||
if (field.type === 'blocks') {
|
||||
;(field.blockReferences ?? field.blocks).forEach((block) => {
|
||||
blocksToDelete.add(toSnakeCase(typeof block === 'string' ? block : block.slug))
|
||||
const matchedBlock =
|
||||
typeof block === 'string'
|
||||
? adapter.payload.config.blocks.find((each) => each.slug === block)
|
||||
: block
|
||||
|
||||
blocksToDelete.add(
|
||||
resolveBlockTableName(
|
||||
matchedBlock,
|
||||
adapter.tableNameMap.get(`${baseTableName}_blocks_${toSnakeCase(matchedBlock.slug)}`),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
if (isLocalized) {
|
||||
|
||||
@@ -28,7 +28,7 @@ export type RowToInsert = {
|
||||
[tableName: string]: ArrayRowToInsert[]
|
||||
}
|
||||
blocks: {
|
||||
[blockType: string]: BlockRowToInsert[]
|
||||
[tableName: string]: BlockRowToInsert[]
|
||||
}
|
||||
blocksToDelete: Set<string>
|
||||
locales: {
|
||||
|
||||
@@ -134,16 +134,16 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
|
||||
// If there are blocks, add parent to each, and then
|
||||
// store by table name and rows
|
||||
Object.keys(rowToInsert.blocks).forEach((blockName) => {
|
||||
rowToInsert.blocks[blockName].forEach((blockRow) => {
|
||||
Object.keys(rowToInsert.blocks).forEach((tableName) => {
|
||||
rowToInsert.blocks[tableName].forEach((blockRow) => {
|
||||
blockRow.row._parentID = insertedRow.id
|
||||
if (!blocksToInsert[blockName]) {
|
||||
blocksToInsert[blockName] = []
|
||||
if (!blocksToInsert[tableName]) {
|
||||
blocksToInsert[tableName] = []
|
||||
}
|
||||
if (blockRow.row.uuid) {
|
||||
delete blockRow.row.uuid
|
||||
}
|
||||
blocksToInsert[blockName].push(blockRow)
|
||||
blocksToInsert[tableName].push(blockRow)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -258,12 +258,11 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
const insertedBlockRows: Record<string, Record<string, unknown>[]> = {}
|
||||
|
||||
if (operation === 'update') {
|
||||
for (const blockName of rowToInsert.blocksToDelete) {
|
||||
const blockTableName = adapter.tableNameMap.get(`${tableName}_blocks_${blockName}`)
|
||||
const blockTable = adapter.tables[blockTableName]
|
||||
for (const tableName of rowToInsert.blocksToDelete) {
|
||||
const blockTable = adapter.tables[tableName]
|
||||
await adapter.deleteWhere({
|
||||
db,
|
||||
tableName: blockTableName,
|
||||
tableName,
|
||||
where: eq(blockTable._parentID, insertedRow.id),
|
||||
})
|
||||
}
|
||||
@@ -272,15 +271,14 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
// When versions are enabled, this is used to track mapping between blocks/arrays ObjectID to their numeric generated representation, then we use it for nested to arrays/blocks select hasMany in versions.
|
||||
const arraysBlocksUUIDMap: Record<string, number | string> = {}
|
||||
|
||||
for (const [blockName, blockRows] of Object.entries(blocksToInsert)) {
|
||||
const blockTableName = adapter.tableNameMap.get(`${tableName}_blocks_${blockName}`)
|
||||
insertedBlockRows[blockName] = await adapter.insert({
|
||||
for (const [tableName, blockRows] of Object.entries(blocksToInsert)) {
|
||||
insertedBlockRows[tableName] = await adapter.insert({
|
||||
db,
|
||||
tableName: blockTableName,
|
||||
tableName,
|
||||
values: blockRows.map(({ row }) => row),
|
||||
})
|
||||
|
||||
insertedBlockRows[blockName].forEach((row, i) => {
|
||||
insertedBlockRows[tableName].forEach((row, i) => {
|
||||
blockRows[i].row = row
|
||||
if (
|
||||
typeof row._uuid === 'string' &&
|
||||
@@ -310,7 +308,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
if (blockLocaleRowsToInsert.length > 0) {
|
||||
await adapter.insert({
|
||||
db,
|
||||
tableName: `${blockTableName}${adapter.localesSuffix}`,
|
||||
tableName: `${tableName}${adapter.localesSuffix}`,
|
||||
values: blockLocaleRowsToInsert,
|
||||
})
|
||||
}
|
||||
@@ -319,7 +317,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
adapter,
|
||||
arrays: blockRows.map(({ arrays }) => arrays),
|
||||
db,
|
||||
parentRows: insertedBlockRows[blockName],
|
||||
parentRows: insertedBlockRows[tableName],
|
||||
uuidMap: arraysBlocksUUIDMap,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,11 @@ export const createBlocksMap = (data: Record<string, unknown>): BlocksMap => {
|
||||
|
||||
Object.entries(data).forEach(([key, rows]) => {
|
||||
if (key.startsWith('_blocks_') && Array.isArray(rows)) {
|
||||
const blockType = key.replace('_blocks_', '')
|
||||
let blockType = key.replace('_blocks_', '')
|
||||
const parsed = blockType.split('_')
|
||||
if (parsed.length === 2 && Number.isInteger(Number(parsed[1]))) {
|
||||
blockType = parsed[0]
|
||||
}
|
||||
|
||||
rows.forEach((row) => {
|
||||
if ('_path' in row) {
|
||||
|
||||
@@ -267,8 +267,11 @@ declare module '${this.packageName}' {
|
||||
*/
|
||||
`
|
||||
|
||||
const importTypes = `import type {} from '${this.packageName}'`
|
||||
|
||||
let code = [
|
||||
warning,
|
||||
importTypes,
|
||||
...importDeclarationsSanitized,
|
||||
schemaDeclaration,
|
||||
...enumDeclarations,
|
||||
|
||||
17
packages/drizzle/src/utilities/getArrayRelationName.ts
Normal file
17
packages/drizzle/src/utilities/getArrayRelationName.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { ArrayField } from 'payload'
|
||||
|
||||
export const getArrayRelationName = ({
|
||||
field,
|
||||
path,
|
||||
tableName,
|
||||
}: {
|
||||
field: ArrayField
|
||||
path: string
|
||||
tableName: string
|
||||
}) => {
|
||||
if (field.dbName && path.length > 63) {
|
||||
return `_${tableName}`
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { deepStrictEqual } from 'assert'
|
||||
import { dequal } from 'dequal'
|
||||
import prompts from 'prompts'
|
||||
|
||||
import type { BasePostgresAdapter } from '../postgres/types.js'
|
||||
@@ -23,18 +23,18 @@ export const pushDevSchema = async (adapter: DrizzleAdapter) => {
|
||||
const localeCodes =
|
||||
adapter.payload.config.localization && adapter.payload.config.localization.localeCodes
|
||||
|
||||
try {
|
||||
deepStrictEqual(previousSchema, {
|
||||
localeCodes,
|
||||
rawTables: adapter.rawTables,
|
||||
})
|
||||
const equal = dequal(previousSchema, {
|
||||
localeCodes,
|
||||
rawTables: adapter.rawTables,
|
||||
})
|
||||
|
||||
if (equal) {
|
||||
if (adapter.logger) {
|
||||
adapter.payload.logger.info('No changes detected in schema, skipping schema push.')
|
||||
}
|
||||
|
||||
return
|
||||
} catch {
|
||||
} else {
|
||||
previousSchema.localeCodes = localeCodes
|
||||
previousSchema.rawTables = adapter.rawTables
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Block, Field } from 'payload'
|
||||
import type { Block, Field, FlattenedBlock } from 'payload'
|
||||
|
||||
import { InvalidConfiguration } from 'payload'
|
||||
import {
|
||||
fieldAffectsData,
|
||||
fieldHasSubFields,
|
||||
@@ -83,14 +82,16 @@ const getFlattenedFieldNames = (args: {
|
||||
}, [])
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if all the fields in a block are identical to the existing table
|
||||
*/
|
||||
export const validateExistingBlockIsIdentical = ({
|
||||
block,
|
||||
localized,
|
||||
parentIsLocalized,
|
||||
rootTableName,
|
||||
table,
|
||||
tableLocales,
|
||||
}: Args): void => {
|
||||
}: Args): boolean => {
|
||||
const fieldNames = getFlattenedFieldNames({
|
||||
fields: block.fields,
|
||||
parentIsLocalized: parentIsLocalized || localized,
|
||||
@@ -110,18 +111,21 @@ export const validateExistingBlockIsIdentical = ({
|
||||
})
|
||||
|
||||
if (missingField) {
|
||||
throw new InvalidConfiguration(
|
||||
`The table ${rootTableName} has multiple blocks with slug ${
|
||||
block.slug
|
||||
}, but the schemas do not match. One block includes the field ${
|
||||
typeof missingField === 'string' ? missingField : missingField.name
|
||||
}, while the other block does not.`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
if (Boolean(localized) !== Boolean(table.columns._locale)) {
|
||||
throw new InvalidConfiguration(
|
||||
`The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One is localized, but another is not. Block schemas of the same name must match exactly.`,
|
||||
)
|
||||
}
|
||||
return Boolean(localized) === Boolean(table.columns._locale)
|
||||
}
|
||||
|
||||
export const InternalBlockTableNameIndex = Symbol('InternalBlockTableNameIndex')
|
||||
export const setInternalBlockIndex = (block: FlattenedBlock, index: number) => {
|
||||
block[InternalBlockTableNameIndex] = index
|
||||
}
|
||||
|
||||
export const resolveBlockTableName = (block: FlattenedBlock, originalTableName: string) => {
|
||||
if (!block[InternalBlockTableNameIndex]) {
|
||||
return originalTableName
|
||||
}
|
||||
|
||||
return `${originalTableName}_${block[InternalBlockTableNameIndex]}`
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.37.0",
|
||||
"version": "3.38.0",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.37.0",
|
||||
"version": "3.38.0",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.37.0",
|
||||
"version": "3.38.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -145,27 +145,37 @@ export function buildMutationInputType({
|
||||
},
|
||||
}),
|
||||
group: (inputObjectTypeConfig: InputObjectTypeConfig, field: GroupField) => {
|
||||
const requiresAtLeastOneField = groupOrTabHasRequiredSubfield(field)
|
||||
const fullName = combineParentName(parentName, toWords(field.name, true))
|
||||
let type: GraphQLType = buildMutationInputType({
|
||||
name: fullName,
|
||||
config,
|
||||
fields: field.fields,
|
||||
graphqlResult,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
parentName: fullName,
|
||||
})
|
||||
if (fieldAffectsData(field)) {
|
||||
const requiresAtLeastOneField = groupOrTabHasRequiredSubfield(field)
|
||||
const fullName = combineParentName(parentName, toWords(field.name, true))
|
||||
let type: GraphQLType = buildMutationInputType({
|
||||
name: fullName,
|
||||
config,
|
||||
fields: field.fields,
|
||||
graphqlResult,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
parentName: fullName,
|
||||
})
|
||||
|
||||
if (!type) {
|
||||
return inputObjectTypeConfig
|
||||
}
|
||||
if (!type) {
|
||||
return inputObjectTypeConfig
|
||||
}
|
||||
|
||||
if (requiresAtLeastOneField) {
|
||||
type = new GraphQLNonNull(type)
|
||||
}
|
||||
return {
|
||||
...inputObjectTypeConfig,
|
||||
[formatName(field.name)]: { type },
|
||||
if (requiresAtLeastOneField) {
|
||||
type = new GraphQLNonNull(type)
|
||||
}
|
||||
return {
|
||||
...inputObjectTypeConfig,
|
||||
[formatName(field.name)]: { type },
|
||||
}
|
||||
} else {
|
||||
return field.fields.reduce((acc, subField: CollapsibleField) => {
|
||||
const addSubField = fieldToSchemaMap[subField.type]
|
||||
if (addSubField) {
|
||||
return addSubField(acc, subField)
|
||||
}
|
||||
return acc
|
||||
}, inputObjectTypeConfig)
|
||||
}
|
||||
},
|
||||
json: (inputObjectTypeConfig: InputObjectTypeConfig, field: JSONField) => ({
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
} from 'graphql'
|
||||
import { DateTimeResolver, EmailAddressResolver } from 'graphql-scalars'
|
||||
import { combineQueries, createDataloaderCacheKey, MissingEditorProp, toWords } from 'payload'
|
||||
import { tabHasName } from 'payload/shared'
|
||||
import { fieldAffectsData, tabHasName } from 'payload/shared'
|
||||
|
||||
import type { Context } from '../resolvers/types.js'
|
||||
|
||||
@@ -302,44 +302,64 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
|
||||
field,
|
||||
forceNullable,
|
||||
graphqlResult,
|
||||
newlyCreatedBlockType,
|
||||
objectTypeConfig,
|
||||
parentIsLocalized,
|
||||
parentName,
|
||||
}) => {
|
||||
const interfaceName =
|
||||
field?.interfaceName || combineParentName(parentName, toWords(field.name, true))
|
||||
if (fieldAffectsData(field)) {
|
||||
const interfaceName =
|
||||
field?.interfaceName || combineParentName(parentName, toWords(field.name, true))
|
||||
|
||||
if (!graphqlResult.types.groupTypes[interfaceName]) {
|
||||
const objectType = buildObjectType({
|
||||
name: interfaceName,
|
||||
config,
|
||||
fields: field.fields,
|
||||
forceNullable: isFieldNullable({ field, forceNullable, parentIsLocalized }),
|
||||
graphqlResult,
|
||||
parentIsLocalized: field.localized || parentIsLocalized,
|
||||
parentName: interfaceName,
|
||||
})
|
||||
if (!graphqlResult.types.groupTypes[interfaceName]) {
|
||||
const objectType = buildObjectType({
|
||||
name: interfaceName,
|
||||
config,
|
||||
fields: field.fields,
|
||||
forceNullable: isFieldNullable({ field, forceNullable, parentIsLocalized }),
|
||||
graphqlResult,
|
||||
parentIsLocalized: field.localized || parentIsLocalized,
|
||||
parentName: interfaceName,
|
||||
})
|
||||
|
||||
if (Object.keys(objectType.getFields()).length) {
|
||||
graphqlResult.types.groupTypes[interfaceName] = objectType
|
||||
if (Object.keys(objectType.getFields()).length) {
|
||||
graphqlResult.types.groupTypes[interfaceName] = objectType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!graphqlResult.types.groupTypes[interfaceName]) {
|
||||
return objectTypeConfig
|
||||
}
|
||||
if (!graphqlResult.types.groupTypes[interfaceName]) {
|
||||
return objectTypeConfig
|
||||
}
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[formatName(field.name)]: {
|
||||
type: graphqlResult.types.groupTypes[interfaceName],
|
||||
resolve: (parent, args, context: Context) => {
|
||||
return {
|
||||
...parent[field.name],
|
||||
_id: parent._id ?? parent.id,
|
||||
}
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[formatName(field.name)]: {
|
||||
type: graphqlResult.types.groupTypes[interfaceName],
|
||||
resolve: (parent, args, context: Context) => {
|
||||
return {
|
||||
...parent[field.name],
|
||||
_id: parent._id ?? parent.id,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return field.fields.reduce((objectTypeConfigWithCollapsibleFields, subField) => {
|
||||
const addSubField: GenericFieldToSchemaMap = fieldToSchemaMap[subField.type]
|
||||
if (addSubField) {
|
||||
return addSubField({
|
||||
config,
|
||||
field: subField,
|
||||
forceNullable,
|
||||
graphqlResult,
|
||||
newlyCreatedBlockType,
|
||||
objectTypeConfig: objectTypeConfigWithCollapsibleFields,
|
||||
parentIsLocalized,
|
||||
parentName,
|
||||
})
|
||||
}
|
||||
return objectTypeConfigWithCollapsibleFields
|
||||
}, objectTypeConfig)
|
||||
}
|
||||
},
|
||||
join: ({ collectionSlug, field, graphqlResult, objectTypeConfig, parentName }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.37.0",
|
||||
"version": "3.38.0",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.37.0",
|
||||
"version": "3.38.0",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.37.0",
|
||||
"version": "3.38.0",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.37.0",
|
||||
"version": "3.38.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,41 +1,36 @@
|
||||
import type { DefaultDocumentIDType, NavPreferences, Payload, User } from 'payload'
|
||||
import type { NavPreferences, PayloadRequest } from 'payload'
|
||||
|
||||
import { cache } from 'react'
|
||||
|
||||
export const getNavPrefs = cache(
|
||||
async (
|
||||
payload: Payload,
|
||||
userID: DefaultDocumentIDType,
|
||||
userSlug: string,
|
||||
): Promise<NavPreferences> => {
|
||||
return userSlug
|
||||
? await payload
|
||||
.find({
|
||||
collection: 'payload-preferences',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
key: {
|
||||
equals: 'nav',
|
||||
},
|
||||
export const getNavPrefs = cache(async (req: PayloadRequest): Promise<NavPreferences> => {
|
||||
return req?.user?.collection
|
||||
? await req.payload
|
||||
.find({
|
||||
collection: 'payload-preferences',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
key: {
|
||||
equals: 'nav',
|
||||
},
|
||||
{
|
||||
'user.relationTo': {
|
||||
equals: userSlug,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.relationTo': {
|
||||
equals: req.user.collection,
|
||||
},
|
||||
{
|
||||
'user.value': {
|
||||
equals: userID,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.value': {
|
||||
equals: req?.user?.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
?.then((res) => res?.docs?.[0]?.value)
|
||||
: null
|
||||
},
|
||||
)
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
?.then((res) => res?.docs?.[0]?.value)
|
||||
: null
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { EntityToGroup } from '@payloadcms/ui/shared'
|
||||
import type { ServerProps } from 'payload'
|
||||
import type { PayloadRequest, ServerProps } from 'payload'
|
||||
|
||||
import { Logout } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
@@ -15,7 +15,9 @@ const baseClass = 'nav'
|
||||
import { getNavPrefs } from './getNavPrefs.js'
|
||||
import { DefaultNavClient } from './index.client.js'
|
||||
|
||||
export type NavProps = ServerProps
|
||||
export type NavProps = {
|
||||
req?: PayloadRequest
|
||||
} & ServerProps
|
||||
|
||||
export const DefaultNav: React.FC<NavProps> = async (props) => {
|
||||
const {
|
||||
@@ -25,6 +27,7 @@ export const DefaultNav: React.FC<NavProps> = async (props) => {
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
req,
|
||||
searchParams,
|
||||
user,
|
||||
viewType,
|
||||
@@ -68,7 +71,7 @@ export const DefaultNav: React.FC<NavProps> = async (props) => {
|
||||
i18n,
|
||||
)
|
||||
|
||||
const navPreferences = await getNavPrefs(payload, user?.id, user?.collection)
|
||||
const navPreferences = await getNavPrefs(req)
|
||||
|
||||
const LogoutComponent = RenderServerComponent({
|
||||
clientProps: {
|
||||
|
||||
@@ -79,7 +79,7 @@ export const RootLayout = async ({
|
||||
})
|
||||
}
|
||||
|
||||
const navPrefs = await getNavPrefs(req.payload, req.user?.id, req.user?.collection)
|
||||
const navPrefs = await getNavPrefs(req)
|
||||
|
||||
const clientConfig = getClientConfig({
|
||||
config,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
CustomComponent,
|
||||
DocumentSubViewTypes,
|
||||
PayloadRequest,
|
||||
ServerProps,
|
||||
ViewTypes,
|
||||
VisibleEntities,
|
||||
@@ -32,6 +33,7 @@ export type DefaultTemplateProps = {
|
||||
docID?: number | string
|
||||
documentSubViewType?: DocumentSubViewTypes
|
||||
globalSlug?: string
|
||||
req?: PayloadRequest
|
||||
viewActions?: CustomComponent[]
|
||||
viewType?: ViewTypes
|
||||
visibleEntities: VisibleEntities
|
||||
@@ -49,6 +51,7 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
req,
|
||||
searchParams,
|
||||
user,
|
||||
viewActions,
|
||||
@@ -84,6 +87,7 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
req,
|
||||
searchParams,
|
||||
user,
|
||||
}),
|
||||
@@ -98,6 +102,7 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
|
||||
globalSlug,
|
||||
collectionSlug,
|
||||
docID,
|
||||
req,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ export const getDocumentData = async ({
|
||||
}: Args): Promise<null | Record<string, unknown> | TypeWithID> => {
|
||||
const id = sanitizeID(idArg)
|
||||
let resolvedData: Record<string, unknown> | TypeWithID = null
|
||||
const { transactionID, ...rest } = req
|
||||
|
||||
try {
|
||||
if (collectionSlug && id) {
|
||||
@@ -41,9 +42,7 @@ export const getDocumentData = async ({
|
||||
locale: locale?.code,
|
||||
overrideAccess: false,
|
||||
req: {
|
||||
query: req?.query,
|
||||
search: req?.search,
|
||||
searchParams: req?.searchParams,
|
||||
...rest,
|
||||
},
|
||||
user,
|
||||
})
|
||||
@@ -58,9 +57,7 @@ export const getDocumentData = async ({
|
||||
locale: locale?.code,
|
||||
overrideAccess: false,
|
||||
req: {
|
||||
query: req?.query,
|
||||
search: req?.search,
|
||||
searchParams: req?.searchParams,
|
||||
...rest,
|
||||
},
|
||||
user,
|
||||
})
|
||||
|
||||
@@ -167,6 +167,7 @@ export const RootPage = async ({
|
||||
params={params}
|
||||
payload={initPageResult?.req.payload}
|
||||
permissions={initPageResult?.permissions}
|
||||
req={initPageResult?.req}
|
||||
searchParams={searchParams}
|
||||
user={initPageResult?.req.user}
|
||||
viewActions={serverProps.viewActions}
|
||||
|
||||
@@ -132,6 +132,7 @@ export const withPayload = (nextConfig = {}, options = {}) => {
|
||||
'drizzle-kit/api',
|
||||
'sharp',
|
||||
'libsql',
|
||||
'require-in-the-middle',
|
||||
],
|
||||
ignoreWarnings: [
|
||||
...(incomingWebpackConfig?.ignoreWarnings || []),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/payload-cloud",
|
||||
"version": "3.37.0",
|
||||
"version": "3.38.0",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -16,6 +16,14 @@ export const generateRandomString = (): string => {
|
||||
return Array.from({ length: 24 }, () => chars[Math.floor(Math.random() * chars.length)]).join('')
|
||||
}
|
||||
|
||||
const DEFAULT_CRON = '* * * * *'
|
||||
const DEFAULT_LIMIT = 10
|
||||
const DEFAULT_CRON_JOB = {
|
||||
cron: DEFAULT_CRON,
|
||||
limit: DEFAULT_LIMIT,
|
||||
queue: 'default',
|
||||
}
|
||||
|
||||
export const payloadCloudPlugin =
|
||||
(pluginOptions?: PluginOptions) =>
|
||||
async (incomingConfig: Config): Promise<Config> => {
|
||||
@@ -100,15 +108,6 @@ export const payloadCloudPlugin =
|
||||
}
|
||||
|
||||
// We make sure to only run cronjobs on one instance using a instance identifier stored in a global.
|
||||
|
||||
const DEFAULT_CRON = '* * * * *'
|
||||
const DEFAULT_LIMIT = 10
|
||||
const DEFAULT_CRON_JOB = {
|
||||
cron: DEFAULT_CRON,
|
||||
limit: DEFAULT_LIMIT,
|
||||
queue: 'default',
|
||||
}
|
||||
|
||||
config.globals = [
|
||||
...(config.globals || []),
|
||||
{
|
||||
@@ -126,13 +125,13 @@ export const payloadCloudPlugin =
|
||||
},
|
||||
]
|
||||
|
||||
if (pluginOptions?.enableAutoRun === false || !config.jobs) {
|
||||
if (pluginOptions?.enableAutoRun === false) {
|
||||
return config
|
||||
}
|
||||
|
||||
const oldAutoRunCopy = config.jobs.autoRun ?? []
|
||||
const oldAutoRunCopy = config.jobs?.autoRun ?? []
|
||||
|
||||
const hasExistingAutorun = Boolean(config.jobs.autoRun)
|
||||
const hasExistingAutorun = Boolean(config.jobs?.autoRun)
|
||||
|
||||
const newShouldAutoRun = async (payload: Payload) => {
|
||||
if (process.env.PAYLOAD_CLOUD_JOBS_INSTANCE) {
|
||||
@@ -150,8 +149,8 @@ export const payloadCloudPlugin =
|
||||
return false
|
||||
}
|
||||
|
||||
if (!config.jobs.shouldAutoRun) {
|
||||
config.jobs.shouldAutoRun = newShouldAutoRun
|
||||
if (!config.jobs?.shouldAutoRun) {
|
||||
;(config.jobs ??= {}).shouldAutoRun = newShouldAutoRun
|
||||
}
|
||||
|
||||
const newAutoRun = async (payload: Payload) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.37.0",
|
||||
"version": "3.38.0",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
|
||||
@@ -65,6 +65,7 @@ export type AfterChangeRichTextHookArgs<
|
||||
/** The previous value of the field, before changes */
|
||||
previousValue?: TValue
|
||||
}
|
||||
|
||||
export type BeforeValidateRichTextHookArgs<
|
||||
TData extends TypeWithID = any,
|
||||
TValue = any,
|
||||
@@ -102,11 +103,11 @@ export type BeforeChangeRichTextHookArgs<
|
||||
mergeLocaleActions?: (() => Promise<void> | void)[]
|
||||
/** A string relating to which operation the field type is currently executing within. */
|
||||
operation?: 'create' | 'delete' | 'read' | 'update'
|
||||
overrideAccess: boolean
|
||||
/** The sibling data of the document before changes being applied. */
|
||||
previousSiblingDoc?: TData
|
||||
/** The previous value of the field, before changes */
|
||||
previousValue?: TValue
|
||||
|
||||
/**
|
||||
* The original siblingData with locales (not modified by any hooks).
|
||||
*/
|
||||
@@ -190,6 +191,7 @@ export type RichTextHooks = {
|
||||
beforeChange?: BeforeChangeRichTextHook[]
|
||||
beforeValidate?: BeforeValidateRichTextHook[]
|
||||
}
|
||||
|
||||
type RichTextAdapterBase<
|
||||
Value extends object = object,
|
||||
AdapterProps = any,
|
||||
|
||||
@@ -91,6 +91,7 @@ export type ServerComponentProps = {
|
||||
req: PayloadRequest
|
||||
siblingData: Data
|
||||
user: User
|
||||
value?: unknown
|
||||
}
|
||||
|
||||
export type ClientFieldBase<
|
||||
|
||||
@@ -28,25 +28,35 @@ const traverseFields = ({
|
||||
break
|
||||
}
|
||||
case 'group': {
|
||||
let targetResult
|
||||
if (typeof field.saveToJWT === 'string') {
|
||||
targetResult = field.saveToJWT
|
||||
result[field.saveToJWT] = data[field.name]
|
||||
} else if (field.saveToJWT) {
|
||||
targetResult = field.name
|
||||
result[field.name] = data[field.name]
|
||||
if (fieldAffectsData(field)) {
|
||||
let targetResult
|
||||
if (typeof field.saveToJWT === 'string') {
|
||||
targetResult = field.saveToJWT
|
||||
result[field.saveToJWT] = data[field.name]
|
||||
} else if (field.saveToJWT) {
|
||||
targetResult = field.name
|
||||
result[field.name] = data[field.name]
|
||||
}
|
||||
const groupData: Record<string, unknown> = data[field.name] as Record<string, unknown>
|
||||
const groupResult = (targetResult ? result[targetResult] : result) as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
traverseFields({
|
||||
data: groupData,
|
||||
fields: field.fields,
|
||||
result: groupResult,
|
||||
})
|
||||
break
|
||||
} else {
|
||||
traverseFields({
|
||||
data,
|
||||
fields: field.fields,
|
||||
result,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
const groupData: Record<string, unknown> = data[field.name] as Record<string, unknown>
|
||||
const groupResult = (targetResult ? result[targetResult] : result) as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
traverseFields({
|
||||
data: groupData,
|
||||
fields: field.fields,
|
||||
result: groupResult,
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'tab': {
|
||||
if (tabHasName(field)) {
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
// @ts-strict-ignore
|
||||
import type { Field } from '../../fields/config/types.js'
|
||||
import type { CollectionConfig } from '../../index.js'
|
||||
|
||||
import { ReservedFieldName } from '../../errors/ReservedFieldName.js'
|
||||
import { fieldAffectsData } from '../../fields/config/types.js'
|
||||
|
||||
// Note for future reference: We've slimmed down the reserved field names but left them in here for reference in case it's needed in the future.
|
||||
|
||||
/**
|
||||
* Reserved field names for collections with auth config enabled
|
||||
*/
|
||||
const reservedBaseAuthFieldNames = [
|
||||
/* 'email',
|
||||
'resetPasswordToken',
|
||||
'resetPasswordExpiration', */
|
||||
'salt',
|
||||
'hash',
|
||||
]
|
||||
/**
|
||||
* Reserved field names for auth collections with verify: true
|
||||
*/
|
||||
const reservedVerifyFieldNames = [
|
||||
/* '_verified', '_verificationToken' */
|
||||
]
|
||||
/**
|
||||
* Reserved field names for auth collections with useApiKey: true
|
||||
*/
|
||||
const reservedAPIKeyFieldNames = [
|
||||
/* 'enableAPIKey', 'apiKeyIndex', 'apiKey' */
|
||||
]
|
||||
|
||||
/**
|
||||
* Reserved field names for collections with upload config enabled
|
||||
*/
|
||||
const reservedBaseUploadFieldNames = [
|
||||
'file',
|
||||
/* 'mimeType',
|
||||
'thumbnailURL',
|
||||
'width',
|
||||
'height',
|
||||
'filesize',
|
||||
'filename',
|
||||
'url',
|
||||
'focalX',
|
||||
'focalY',
|
||||
'sizes', */
|
||||
]
|
||||
|
||||
/**
|
||||
* Reserved field names for collections with versions enabled
|
||||
*/
|
||||
const reservedVersionsFieldNames = [
|
||||
/* '__v', '_status' */
|
||||
]
|
||||
|
||||
/**
|
||||
* Sanitize fields for collections with auth config enabled.
|
||||
*
|
||||
* Should run on top level fields only.
|
||||
*/
|
||||
export const sanitizeAuthFields = (fields: Field[], config: CollectionConfig) => {
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const field = fields[i]
|
||||
|
||||
if (fieldAffectsData(field) && field.name) {
|
||||
if (config.auth && typeof config.auth === 'object' && !config.auth.disableLocalStrategy) {
|
||||
const auth = config.auth
|
||||
|
||||
if (reservedBaseAuthFieldNames.includes(field.name)) {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
|
||||
if (auth.verify) {
|
||||
if (reservedAPIKeyFieldNames.includes(field.name)) {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
}
|
||||
|
||||
/* if (auth.maxLoginAttempts) {
|
||||
if (field.name === 'loginAttempts' || field.name === 'lockUntil') {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
} */
|
||||
|
||||
/* if (auth.loginWithUsername) {
|
||||
if (field.name === 'username') {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
} */
|
||||
|
||||
if (auth.verify) {
|
||||
if (reservedVerifyFieldNames.includes(field.name)) {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tabs without a name
|
||||
if (field.type === 'tabs') {
|
||||
for (let j = 0; j < field.tabs.length; j++) {
|
||||
const tab = field.tabs[j]
|
||||
|
||||
if (!('name' in tab)) {
|
||||
sanitizeAuthFields(tab.fields, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle presentational fields like rows and collapsibles
|
||||
if (!fieldAffectsData(field) && 'fields' in field && field.fields) {
|
||||
sanitizeAuthFields(field.fields, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize fields for collections with upload config enabled.
|
||||
*
|
||||
* Should run on top level fields only.
|
||||
*/
|
||||
export const sanitizeUploadFields = (fields: Field[], config: CollectionConfig) => {
|
||||
if (config.upload && typeof config.upload === 'object') {
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const field = fields[i]
|
||||
|
||||
if (fieldAffectsData(field) && field.name) {
|
||||
if (reservedBaseUploadFieldNames.includes(field.name)) {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tabs without a name
|
||||
if (field.type === 'tabs') {
|
||||
for (let j = 0; j < field.tabs.length; j++) {
|
||||
const tab = field.tabs[j]
|
||||
|
||||
if (!('name' in tab)) {
|
||||
sanitizeUploadFields(tab.fields, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle presentational fields like rows and collapsibles
|
||||
if (!fieldAffectsData(field) && 'fields' in field && field.fields) {
|
||||
sanitizeUploadFields(field.fields, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
addDefaultsToCollectionConfig,
|
||||
addDefaultsToLoginWithUsernameConfig,
|
||||
} from './defaults.js'
|
||||
import { sanitizeAuthFields, sanitizeUploadFields } from './reservedFieldNames.js'
|
||||
import { sanitizeCompoundIndexes } from './sanitizeCompoundIndexes.js'
|
||||
import { validateUseAsTitle } from './useAsTitle.js'
|
||||
|
||||
@@ -43,7 +42,9 @@ export const sanitizeCollection = async (
|
||||
if (collection._sanitized) {
|
||||
return collection as SanitizedCollectionConfig
|
||||
}
|
||||
|
||||
collection._sanitized = true
|
||||
|
||||
// /////////////////////////////////
|
||||
// Make copy of collection config
|
||||
// /////////////////////////////////
|
||||
@@ -57,7 +58,9 @@ export const sanitizeCollection = async (
|
||||
const validRelationships = _validRelationships ?? config.collections.map((c) => c.slug) ?? []
|
||||
|
||||
const joins: SanitizedJoins = {}
|
||||
|
||||
const polymorphicJoins: SanitizedJoin[] = []
|
||||
|
||||
sanitized.fields = await sanitizeFields({
|
||||
collectionConfig: sanitized,
|
||||
config,
|
||||
@@ -96,17 +99,21 @@ export const sanitizeCollection = async (
|
||||
// add default timestamps fields only as needed
|
||||
let hasUpdatedAt: boolean | null = null
|
||||
let hasCreatedAt: boolean | null = null
|
||||
|
||||
sanitized.fields.some((field) => {
|
||||
if (fieldAffectsData(field)) {
|
||||
if (field.name === 'updatedAt') {
|
||||
hasUpdatedAt = true
|
||||
}
|
||||
|
||||
if (field.name === 'createdAt') {
|
||||
hasCreatedAt = true
|
||||
}
|
||||
}
|
||||
|
||||
return hasCreatedAt && hasUpdatedAt
|
||||
})
|
||||
|
||||
if (!hasUpdatedAt) {
|
||||
sanitized.fields.push({
|
||||
name: 'updatedAt',
|
||||
@@ -119,6 +126,7 @@ export const sanitizeCollection = async (
|
||||
label: ({ t }) => t('general:updatedAt'),
|
||||
})
|
||||
}
|
||||
|
||||
if (!hasCreatedAt) {
|
||||
sanitized.fields.push({
|
||||
name: 'createdAt',
|
||||
@@ -175,9 +183,6 @@ export const sanitizeCollection = async (
|
||||
sanitized.upload = {}
|
||||
}
|
||||
|
||||
// sanitize fields for reserved names
|
||||
sanitizeUploadFields(sanitized.fields, sanitized)
|
||||
|
||||
sanitized.upload.cacheTags = sanitized.upload?.cacheTags ?? true
|
||||
sanitized.upload.bulkUpload = sanitized.upload?.bulkUpload ?? true
|
||||
sanitized.upload.staticDir = sanitized.upload.staticDir || sanitized.slug
|
||||
@@ -195,9 +200,6 @@ export const sanitizeCollection = async (
|
||||
}
|
||||
|
||||
if (sanitized.auth) {
|
||||
// sanitize fields for reserved names
|
||||
sanitizeAuthFields(sanitized.fields, sanitized)
|
||||
|
||||
sanitized.auth = addDefaultsToAuthConfig(
|
||||
typeof sanitized.auth === 'boolean' ? {} : sanitized.auth,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CollectionConfig } from '../../index.js'
|
||||
|
||||
import { InvalidConfiguration } from '../../errors/InvalidConfiguration.js'
|
||||
import { fieldAffectsData, fieldIsVirtual } from '../../fields/config/types.js'
|
||||
import { fieldAffectsData } from '../../fields/config/types.js'
|
||||
import flattenFields from '../../utilities/flattenTopLevelFields.js'
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,9 +9,6 @@ import type {
|
||||
TransformCollectionWithSelect,
|
||||
} from '../../types/index.js'
|
||||
import type {
|
||||
AfterChangeHook,
|
||||
BeforeOperationHook,
|
||||
BeforeValidateHook,
|
||||
Collection,
|
||||
DataFromCollectionSlug,
|
||||
RequiredDataFromCollectionSlug,
|
||||
@@ -225,6 +222,7 @@ export const createOperation = async <
|
||||
docWithLocales: duplicatedFromDocWithLocales,
|
||||
global: null,
|
||||
operation: 'create',
|
||||
overrideAccess,
|
||||
req,
|
||||
skipValidation:
|
||||
shouldSaveDraft &&
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { AccessResult } from '../../config/types.js'
|
||||
import type { CollectionSlug } from '../../index.js'
|
||||
import type { PayloadRequest, PopulateType, SelectType, Where } from '../../types/index.js'
|
||||
import type {
|
||||
BeforeOperationHook,
|
||||
BulkOperationResult,
|
||||
Collection,
|
||||
DataFromCollectionSlug,
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
SelectType,
|
||||
TransformCollectionWithSelect,
|
||||
} from '../../types/index.js'
|
||||
import type { BeforeOperationHook, Collection, DataFromCollectionSlug } from '../config/types.js'
|
||||
import type { Collection, DataFromCollectionSlug } from '../config/types.js'
|
||||
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { hasWhereAccessResult } from '../../auth/types.js'
|
||||
|
||||
@@ -139,6 +139,7 @@ export default async function createLocal<
|
||||
select,
|
||||
showHiddenFields,
|
||||
} = options
|
||||
|
||||
const collection = payload.collections[collectionSlug]
|
||||
|
||||
if (!collection) {
|
||||
@@ -148,6 +149,7 @@ export default async function createLocal<
|
||||
}
|
||||
|
||||
const req = await createLocalReq(options, payload)
|
||||
|
||||
req.file = file ?? (await getFileByPath(filePath))
|
||||
|
||||
return createOperation<TSlug, TSelect>({
|
||||
|
||||
@@ -109,6 +109,7 @@ export async function duplicate<
|
||||
select,
|
||||
showHiddenFields,
|
||||
} = options
|
||||
|
||||
const collection = payload.collections[collectionSlug]
|
||||
|
||||
if (!collection) {
|
||||
|
||||
@@ -234,6 +234,7 @@ export const updateDocument = async <
|
||||
docWithLocales: undefined,
|
||||
global: null,
|
||||
operation: 'update',
|
||||
overrideAccess,
|
||||
req,
|
||||
skipValidation:
|
||||
shouldSaveDraft &&
|
||||
|
||||
@@ -58,6 +58,7 @@ const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig>
|
||||
// add default user collection if none provided
|
||||
if (!sanitizedConfig?.admin?.user) {
|
||||
const firstCollectionWithAuth = sanitizedConfig.collections.find(({ auth }) => Boolean(auth))
|
||||
|
||||
if (firstCollectionWithAuth) {
|
||||
sanitizedConfig.admin.user = firstCollectionWithAuth.slug
|
||||
} else {
|
||||
@@ -69,6 +70,7 @@ const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig>
|
||||
const userCollection = sanitizedConfig.collections.find(
|
||||
({ slug }) => slug === sanitizedConfig.admin.user,
|
||||
)
|
||||
|
||||
if (!userCollection || !userCollection.auth) {
|
||||
throw new InvalidConfiguration(
|
||||
`${sanitizedConfig.admin.user} is not a valid admin user collection`,
|
||||
|
||||
1
packages/payload/src/exports/i18n/lv.ts
Normal file
1
packages/payload/src/exports/i18n/lv.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { lv } from '@payloadcms/translations/languages/lv'
|
||||
@@ -2,7 +2,7 @@ import type { Config } from '../../config/types.js'
|
||||
import type { CollectionConfig, Field } from '../../index.js'
|
||||
|
||||
import { ReservedFieldName } from '../../errors/index.js'
|
||||
import { sanitizeCollection } from './sanitize.js'
|
||||
import { sanitizeCollection } from '../../collections/config/sanitize.js'
|
||||
|
||||
describe('reservedFieldNames - collections -', () => {
|
||||
const config = {
|
||||
@@ -25,6 +25,7 @@ describe('reservedFieldNames - collections -', () => {
|
||||
label: 'some-collection',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
@@ -53,6 +54,7 @@ describe('reservedFieldNames - collections -', () => {
|
||||
label: 'some-collection',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
@@ -93,6 +95,7 @@ describe('reservedFieldNames - collections -', () => {
|
||||
label: 'some-collection',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
@@ -121,6 +124,7 @@ describe('reservedFieldNames - collections -', () => {
|
||||
label: 'some-collection',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
@@ -149,6 +153,7 @@ describe('reservedFieldNames - collections -', () => {
|
||||
label: 'some-collection',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
48
packages/payload/src/fields/config/reservedFieldNames.ts
Normal file
48
packages/payload/src/fields/config/reservedFieldNames.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Reserved field names for collections with auth config enabled
|
||||
*/
|
||||
export const reservedBaseAuthFieldNames = [
|
||||
/* 'email',
|
||||
'resetPasswordToken',
|
||||
'resetPasswordExpiration', */
|
||||
'salt',
|
||||
'hash',
|
||||
]
|
||||
|
||||
/**
|
||||
* Reserved field names for auth collections with verify: true
|
||||
*/
|
||||
export const reservedVerifyFieldNames = [
|
||||
/* '_verified', '_verificationToken' */
|
||||
]
|
||||
|
||||
/**
|
||||
* Reserved field names for auth collections with useApiKey: true
|
||||
*/
|
||||
export const reservedAPIKeyFieldNames = [
|
||||
/* 'enableAPIKey', 'apiKeyIndex', 'apiKey' */
|
||||
]
|
||||
|
||||
/**
|
||||
* Reserved field names for collections with upload config enabled
|
||||
*/
|
||||
export const reservedBaseUploadFieldNames = [
|
||||
'file',
|
||||
/* 'mimeType',
|
||||
'thumbnailURL',
|
||||
'width',
|
||||
'height',
|
||||
'filesize',
|
||||
'filename',
|
||||
'url',
|
||||
'focalX',
|
||||
'focalY',
|
||||
'sizes', */
|
||||
]
|
||||
|
||||
/**
|
||||
* Reserved field names for collections with versions enabled
|
||||
*/
|
||||
export const reservedVersionsFieldNames = [
|
||||
/* '__v', '_status' */
|
||||
]
|
||||
@@ -11,9 +11,12 @@ import type {
|
||||
|
||||
import { InvalidFieldName, InvalidFieldRelationship, MissingFieldType } from '../../errors/index.js'
|
||||
import { sanitizeFields } from './sanitize.js'
|
||||
import { CollectionConfig } from '../../index.js'
|
||||
|
||||
describe('sanitizeFields', () => {
|
||||
const config = {} as Config
|
||||
const collectionConfig = {} as CollectionConfig
|
||||
|
||||
it('should throw on missing type field', async () => {
|
||||
const fields: Field[] = [
|
||||
// @ts-expect-error
|
||||
@@ -22,14 +25,17 @@ describe('sanitizeFields', () => {
|
||||
label: 'some-collection',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
}).rejects.toThrow(MissingFieldType)
|
||||
})
|
||||
|
||||
it('should throw on invalid field name', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
@@ -38,9 +44,11 @@ describe('sanitizeFields', () => {
|
||||
label: 'some.collection',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
@@ -55,17 +63,21 @@ describe('sanitizeFields', () => {
|
||||
type: 'text',
|
||||
},
|
||||
]
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
)[0] as TextField
|
||||
|
||||
expect(sanitizedField.name).toStrictEqual('someField')
|
||||
expect(sanitizedField.label).toStrictEqual('Some Field')
|
||||
expect(sanitizedField.type).toStrictEqual('text')
|
||||
})
|
||||
|
||||
it('should allow auto-label override', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
@@ -74,13 +86,16 @@ describe('sanitizeFields', () => {
|
||||
label: 'Do not label',
|
||||
},
|
||||
]
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
)[0] as TextField
|
||||
|
||||
expect(sanitizedField.name).toStrictEqual('someField')
|
||||
expect(sanitizedField.label).toStrictEqual('Do not label')
|
||||
expect(sanitizedField.type).toStrictEqual('text')
|
||||
@@ -95,13 +110,16 @@ describe('sanitizeFields', () => {
|
||||
label: false,
|
||||
},
|
||||
]
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
)[0] as TextField
|
||||
|
||||
expect(sanitizedField.name).toStrictEqual('someField')
|
||||
expect(sanitizedField.label).toStrictEqual(false)
|
||||
expect(sanitizedField.type).toStrictEqual('text')
|
||||
@@ -119,18 +137,22 @@ describe('sanitizeFields', () => {
|
||||
],
|
||||
label: false,
|
||||
}
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields: [arrayField],
|
||||
validRelationships: [],
|
||||
})
|
||||
)[0] as ArrayField
|
||||
|
||||
expect(sanitizedField.name).toStrictEqual('items')
|
||||
expect(sanitizedField.label).toStrictEqual(false)
|
||||
expect(sanitizedField.type).toStrictEqual('array')
|
||||
expect(sanitizedField.labels).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow label opt-out for blocks', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
@@ -150,13 +172,16 @@ describe('sanitizeFields', () => {
|
||||
label: false,
|
||||
},
|
||||
]
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
)[0] as BlocksField
|
||||
|
||||
expect(sanitizedField.name).toStrictEqual('noLabelBlock')
|
||||
expect(sanitizedField.label).toStrictEqual(false)
|
||||
expect(sanitizedField.type).toStrictEqual('blocks')
|
||||
@@ -177,13 +202,16 @@ describe('sanitizeFields', () => {
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
)[0] as ArrayField
|
||||
|
||||
expect(sanitizedField.name).toStrictEqual('items')
|
||||
expect(sanitizedField.label).toStrictEqual('Items')
|
||||
expect(sanitizedField.type).toStrictEqual('array')
|
||||
@@ -203,13 +231,16 @@ describe('sanitizeFields', () => {
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
)[0] as BlocksField
|
||||
|
||||
expect(sanitizedField.name).toStrictEqual('specialBlock')
|
||||
expect(sanitizedField.label).toStrictEqual('Special Block')
|
||||
expect(sanitizedField.type).toStrictEqual('blocks')
|
||||
@@ -217,6 +248,7 @@ describe('sanitizeFields', () => {
|
||||
plural: 'Special Blocks',
|
||||
singular: 'Special Block',
|
||||
})
|
||||
|
||||
expect((sanitizedField.blocks[0].fields[0] as NumberField).label).toStrictEqual('Test Number')
|
||||
})
|
||||
})
|
||||
@@ -232,8 +264,9 @@ describe('sanitizeFields', () => {
|
||||
relationTo: 'some-collection',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
await sanitizeFields({ config, collectionConfig, fields, validRelationships })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
@@ -247,8 +280,9 @@ describe('sanitizeFields', () => {
|
||||
relationTo: ['some-collection', 'another-collection'],
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
await sanitizeFields({ config, collectionConfig, fields, validRelationships })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
@@ -265,6 +299,7 @@ describe('sanitizeFields', () => {
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const fields: Field[] = [
|
||||
{
|
||||
name: 'layout',
|
||||
@@ -273,8 +308,9 @@ describe('sanitizeFields', () => {
|
||||
label: 'Layout Blocks',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
await sanitizeFields({ config, collectionConfig, fields, validRelationships })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
@@ -288,8 +324,9 @@ describe('sanitizeFields', () => {
|
||||
relationTo: 'not-valid',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
await sanitizeFields({ config, collectionConfig, fields, validRelationships })
|
||||
}).rejects.toThrow(InvalidFieldRelationship)
|
||||
})
|
||||
|
||||
@@ -303,8 +340,9 @@ describe('sanitizeFields', () => {
|
||||
relationTo: ['some-collection', 'not-valid'],
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
await sanitizeFields({ config, collectionConfig, fields, validRelationships })
|
||||
}).rejects.toThrow(InvalidFieldRelationship)
|
||||
})
|
||||
|
||||
@@ -321,6 +359,7 @@ describe('sanitizeFields', () => {
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const fields: Field[] = [
|
||||
{
|
||||
name: 'layout',
|
||||
@@ -329,8 +368,9 @@ describe('sanitizeFields', () => {
|
||||
label: 'Layout Blocks',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
await sanitizeFields({ config, collectionConfig, fields, validRelationships })
|
||||
}).rejects.toThrow(InvalidFieldRelationship)
|
||||
})
|
||||
|
||||
@@ -346,19 +386,23 @@ describe('sanitizeFields', () => {
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
)[0] as CheckboxField
|
||||
|
||||
expect(sanitizedField.defaultValue).toStrictEqual(false)
|
||||
})
|
||||
|
||||
it('should return empty field array if no fields', async () => {
|
||||
const sanitizedFields = await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields: [],
|
||||
validRelationships: [],
|
||||
})
|
||||
|
||||
expect(sanitizedFields).toStrictEqual([])
|
||||
})
|
||||
})
|
||||
@@ -385,9 +429,11 @@ describe('sanitizeFields', () => {
|
||||
label: false,
|
||||
},
|
||||
]
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
@@ -416,9 +462,11 @@ describe('sanitizeFields', () => {
|
||||
label: false,
|
||||
},
|
||||
]
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
MissingEditorProp,
|
||||
MissingFieldType,
|
||||
} from '../../errors/index.js'
|
||||
import { ReservedFieldName } from '../../errors/ReservedFieldName.js'
|
||||
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
|
||||
import { baseBlockFields } from '../baseFields/baseBlockFields.js'
|
||||
import { baseIDField } from '../baseFields/baseIDField.js'
|
||||
@@ -24,14 +25,24 @@ import { baseTimezoneField } from '../baseFields/timezone/baseField.js'
|
||||
import { defaultTimezones } from '../baseFields/timezone/defaultTimezones.js'
|
||||
import { setDefaultBeforeDuplicate } from '../setDefaultBeforeDuplicate.js'
|
||||
import { validations } from '../validations.js'
|
||||
import {
|
||||
reservedAPIKeyFieldNames,
|
||||
reservedBaseAuthFieldNames,
|
||||
reservedBaseUploadFieldNames,
|
||||
reservedVerifyFieldNames,
|
||||
} from './reservedFieldNames.js'
|
||||
import { sanitizeJoinField } from './sanitizeJoinField.js'
|
||||
import { fieldAffectsData, fieldIsLocalized, tabHasName } from './types.js'
|
||||
import { fieldAffectsData as _fieldAffectsData, fieldIsLocalized, tabHasName } from './types.js'
|
||||
|
||||
type Args = {
|
||||
collectionConfig?: CollectionConfig
|
||||
config: Config
|
||||
existingFieldNames?: Set<string>
|
||||
fields: Field[]
|
||||
/**
|
||||
* Used to prevent unnecessary sanitization of fields that are not top-level.
|
||||
*/
|
||||
isTopLevelField?: boolean
|
||||
joinPath?: string
|
||||
/**
|
||||
* When not passed in, assume that join are not supported (globals, arrays, blocks)
|
||||
@@ -39,7 +50,6 @@ type Args = {
|
||||
joins?: SanitizedJoins
|
||||
parentIsLocalized: boolean
|
||||
polymorphicJoins?: SanitizedJoin[]
|
||||
|
||||
/**
|
||||
* If true, a richText field will require an editor property to be set, as the sanitizeFields function will not add it from the payload config if not present.
|
||||
*
|
||||
@@ -59,9 +69,11 @@ type Args = {
|
||||
}
|
||||
|
||||
export const sanitizeFields = async ({
|
||||
collectionConfig,
|
||||
config,
|
||||
existingFieldNames = new Set(),
|
||||
fields,
|
||||
isTopLevelField = true,
|
||||
joinPath = '',
|
||||
joins,
|
||||
parentIsLocalized,
|
||||
@@ -80,6 +92,7 @@ export const sanitizeFields = async ({
|
||||
if ('_sanitized' in field && field._sanitized === true) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ('_sanitized' in field) {
|
||||
field._sanitized = true
|
||||
}
|
||||
@@ -88,8 +101,39 @@ export const sanitizeFields = async ({
|
||||
throw new MissingFieldType(field)
|
||||
}
|
||||
|
||||
const fieldAffectsData = _fieldAffectsData(field)
|
||||
|
||||
if (isTopLevelField && fieldAffectsData && field.name) {
|
||||
if (collectionConfig && collectionConfig.upload) {
|
||||
if (reservedBaseUploadFieldNames.includes(field.name)) {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
collectionConfig &&
|
||||
collectionConfig.auth &&
|
||||
typeof collectionConfig.auth === 'object' &&
|
||||
!collectionConfig.auth.disableLocalStrategy
|
||||
) {
|
||||
if (reservedBaseAuthFieldNames.includes(field.name)) {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
|
||||
if (collectionConfig.auth.verify) {
|
||||
if (reservedAPIKeyFieldNames.includes(field.name)) {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
|
||||
if (reservedVerifyFieldNames.includes(field.name)) {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// assert that field names do not contain forbidden characters
|
||||
if (fieldAffectsData(field) && field.name.includes('.')) {
|
||||
if (fieldAffectsData && field.name.includes('.')) {
|
||||
throw new InvalidFieldName(field, field.name)
|
||||
}
|
||||
|
||||
@@ -122,6 +166,7 @@ export const sanitizeFields = async ({
|
||||
const relationships = Array.isArray(field.relationTo)
|
||||
? field.relationTo
|
||||
: [field.relationTo]
|
||||
|
||||
relationships.forEach((relationship: string) => {
|
||||
if (!validRelationships.includes(relationship)) {
|
||||
throw new InvalidFieldRelationship(field, relationship)
|
||||
@@ -135,6 +180,7 @@ export const sanitizeFields = async ({
|
||||
)
|
||||
field.minRows = field.min
|
||||
}
|
||||
|
||||
if (field.max && !field.maxRows) {
|
||||
console.warn(
|
||||
`(payload): The "max" property is deprecated for the Relationship field "${field.name}" and will be removed in a future version. Please use "maxRows" instead.`,
|
||||
@@ -160,7 +206,7 @@ export const sanitizeFields = async ({
|
||||
field.labels = field.labels || formatLabels(field.name)
|
||||
}
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
if (fieldAffectsData) {
|
||||
if (existingFieldNames.has(field.name)) {
|
||||
throw new DuplicateFieldName(field.name)
|
||||
} else if (!['blockName', 'id'].includes(field.name)) {
|
||||
@@ -254,9 +300,11 @@ export const sanitizeFields = async ({
|
||||
block.fields = block.fields.concat(baseBlockFields)
|
||||
block.labels = !block.labels ? formatLabels(block.slug) : block.labels
|
||||
block.fields = await sanitizeFields({
|
||||
collectionConfig,
|
||||
config,
|
||||
existingFieldNames: new Set(),
|
||||
fields: block.fields,
|
||||
isTopLevelField: false,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
requireFieldLevelRichTextEditor,
|
||||
richTextSanitizationPromises,
|
||||
@@ -267,12 +315,12 @@ export const sanitizeFields = async ({
|
||||
|
||||
if ('fields' in field && field.fields) {
|
||||
field.fields = await sanitizeFields({
|
||||
collectionConfig,
|
||||
config,
|
||||
existingFieldNames: fieldAffectsData(field) ? new Set() : existingFieldNames,
|
||||
existingFieldNames: fieldAffectsData ? new Set() : existingFieldNames,
|
||||
fields: field.fields,
|
||||
joinPath: fieldAffectsData(field)
|
||||
? `${joinPath ? joinPath + '.' : ''}${field.name}`
|
||||
: joinPath,
|
||||
isTopLevelField: isTopLevelField && !fieldAffectsData,
|
||||
joinPath: fieldAffectsData ? `${joinPath ? joinPath + '.' : ''}${field.name}` : joinPath,
|
||||
joins,
|
||||
parentIsLocalized: parentIsLocalized || fieldIsLocalized(field),
|
||||
polymorphicJoins,
|
||||
@@ -285,7 +333,10 @@ export const sanitizeFields = async ({
|
||||
if (field.type === 'tabs') {
|
||||
for (let j = 0; j < field.tabs.length; j++) {
|
||||
const tab = field.tabs[j]
|
||||
if (tabHasName(tab) && typeof tab.label === 'undefined') {
|
||||
|
||||
const isNamedTab = tabHasName(tab)
|
||||
|
||||
if (isNamedTab && typeof tab.label === 'undefined') {
|
||||
tab.label = toWords(tab.name)
|
||||
}
|
||||
|
||||
@@ -296,21 +347,24 @@ export const sanitizeFields = async ({
|
||||
!tab.id
|
||||
) {
|
||||
// Always attach a UUID to tabs with a condition so there's no conflicts even if there are duplicate nested names
|
||||
tab.id = tabHasName(tab) ? `${tab.name}_${uuid()}` : uuid()
|
||||
tab.id = isNamedTab ? `${tab.name}_${uuid()}` : uuid()
|
||||
}
|
||||
|
||||
tab.fields = await sanitizeFields({
|
||||
collectionConfig,
|
||||
config,
|
||||
existingFieldNames: tabHasName(tab) ? new Set() : existingFieldNames,
|
||||
existingFieldNames: isNamedTab ? new Set() : existingFieldNames,
|
||||
fields: tab.fields,
|
||||
joinPath: tabHasName(tab) ? `${joinPath ? joinPath + '.' : ''}${tab.name}` : joinPath,
|
||||
isTopLevelField: isTopLevelField && !isNamedTab,
|
||||
joinPath: isNamedTab ? `${joinPath ? joinPath + '.' : ''}${tab.name}` : joinPath,
|
||||
joins,
|
||||
parentIsLocalized: parentIsLocalized || (tabHasName(tab) && tab.localized),
|
||||
parentIsLocalized: parentIsLocalized || (isNamedTab && tab.localized),
|
||||
polymorphicJoins,
|
||||
requireFieldLevelRichTextEditor,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
field.tabs[j] = tab
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +404,6 @@ export type LabelsClient = {
|
||||
}
|
||||
|
||||
export type BaseValidateOptions<TData, TSiblingData, TValue> = {
|
||||
/**
|
||||
/**
|
||||
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
|
||||
*/
|
||||
@@ -414,6 +413,10 @@ export type BaseValidateOptions<TData, TSiblingData, TValue> = {
|
||||
event?: 'onChange' | 'submit'
|
||||
id?: number | string
|
||||
operation?: Operation
|
||||
/**
|
||||
* The `overrideAccess` flag that was attached to the request. This is used to bypass access control checks for fields.
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
/**
|
||||
* The path of the field, e.g. ["group", "myArray", 1, "textField"]. The path is the schemaPath but with indexes and would be used in the context of field data, not field schemas.
|
||||
*/
|
||||
@@ -716,7 +719,7 @@ export type DateFieldClient = {
|
||||
} & FieldBaseClient &
|
||||
Pick<DateField, 'timezone' | 'type'>
|
||||
|
||||
export type GroupField = {
|
||||
export type GroupBase = {
|
||||
admin?: {
|
||||
components?: {
|
||||
afterInput?: CustomComponent[]
|
||||
@@ -726,6 +729,11 @@ export type GroupField = {
|
||||
hideGutter?: boolean
|
||||
} & Admin
|
||||
fields: Field[]
|
||||
type: 'group'
|
||||
validate?: Validate<unknown, unknown, unknown, GroupField>
|
||||
} & Omit<FieldBase, 'validate'>
|
||||
|
||||
export type NamedGroupField = {
|
||||
/** Customize generated GraphQL and Typescript schema names.
|
||||
* By default, it is bound to the collection.
|
||||
*
|
||||
@@ -733,15 +741,39 @@ export type GroupField = {
|
||||
* **Note**: Top level types can collide, ensure they are unique amongst collections, arrays, groups, blocks, tabs.
|
||||
*/
|
||||
interfaceName?: string
|
||||
type: 'group'
|
||||
validate?: Validate<unknown, unknown, unknown, GroupField>
|
||||
} & Omit<FieldBase, 'required' | 'validate'>
|
||||
} & GroupBase
|
||||
|
||||
export type GroupFieldClient = {
|
||||
admin?: AdminClient & Pick<GroupField['admin'], 'hideGutter'>
|
||||
export type UnnamedGroupField = {
|
||||
interfaceName?: never
|
||||
/**
|
||||
* Can be either:
|
||||
* - A string, which will be used as the tab's label.
|
||||
* - An object, where the key is the language code and the value is the label.
|
||||
*/
|
||||
label:
|
||||
| {
|
||||
[selectedLanguage: string]: string
|
||||
}
|
||||
| LabelFunction
|
||||
| string
|
||||
localized?: never
|
||||
} & Omit<GroupBase, 'name' | 'virtual'>
|
||||
|
||||
export type GroupField = NamedGroupField | UnnamedGroupField
|
||||
|
||||
export type NamedGroupFieldClient = {
|
||||
admin?: AdminClient & Pick<NamedGroupField['admin'], 'hideGutter'>
|
||||
fields: ClientField[]
|
||||
} & Omit<FieldBaseClient, 'required'> &
|
||||
Pick<GroupField, 'interfaceName' | 'type'>
|
||||
Pick<NamedGroupField, 'interfaceName' | 'type'>
|
||||
|
||||
export type UnnamedGroupFieldClient = {
|
||||
admin?: AdminClient & Pick<UnnamedGroupField['admin'], 'hideGutter'>
|
||||
fields: ClientField[]
|
||||
} & Omit<FieldBaseClient, 'required'> &
|
||||
Pick<UnnamedGroupField, 'label' | 'type'>
|
||||
|
||||
export type GroupFieldClient = NamedGroupFieldClient | UnnamedGroupFieldClient
|
||||
|
||||
export type RowField = {
|
||||
admin?: Omit<Admin, 'description'>
|
||||
@@ -1176,7 +1208,7 @@ export type PolymorphicRelationshipField = {
|
||||
|
||||
export type PolymorphicRelationshipFieldClient = {
|
||||
admin?: {
|
||||
sortOptions?: Pick<PolymorphicRelationshipField['admin'], 'sortOptions'>
|
||||
sortOptions?: PolymorphicRelationshipField['admin']['sortOptions']
|
||||
} & RelationshipAdminClient
|
||||
} & Pick<PolymorphicRelationshipField, 'relationTo'> &
|
||||
SharedRelationshipPropertiesClient
|
||||
@@ -1608,6 +1640,7 @@ export type FlattenedBlocksField = {
|
||||
|
||||
export type FlattenedGroupField = {
|
||||
flattenedFields: FlattenedField[]
|
||||
name: string
|
||||
} & GroupField
|
||||
|
||||
export type FlattenedArrayField = {
|
||||
@@ -1725,9 +1758,9 @@ export type FieldAffectingData =
|
||||
| CodeField
|
||||
| DateField
|
||||
| EmailField
|
||||
| GroupField
|
||||
| JoinField
|
||||
| JSONField
|
||||
| NamedGroupField
|
||||
| NumberField
|
||||
| PointField
|
||||
| RadioField
|
||||
@@ -1746,9 +1779,9 @@ export type FieldAffectingDataClient =
|
||||
| CodeFieldClient
|
||||
| DateFieldClient
|
||||
| EmailFieldClient
|
||||
| GroupFieldClient
|
||||
| JoinFieldClient
|
||||
| JSONFieldClient
|
||||
| NamedGroupFieldClient
|
||||
| NumberFieldClient
|
||||
| PointFieldClient
|
||||
| RadioFieldClient
|
||||
@@ -1768,8 +1801,8 @@ export type NonPresentationalField =
|
||||
| CollapsibleField
|
||||
| DateField
|
||||
| EmailField
|
||||
| GroupField
|
||||
| JSONField
|
||||
| NamedGroupField
|
||||
| NumberField
|
||||
| PointField
|
||||
| RadioField
|
||||
@@ -1790,8 +1823,8 @@ export type NonPresentationalFieldClient =
|
||||
| CollapsibleFieldClient
|
||||
| DateFieldClient
|
||||
| EmailFieldClient
|
||||
| GroupFieldClient
|
||||
| JSONFieldClient
|
||||
| NamedGroupFieldClient
|
||||
| NumberFieldClient
|
||||
| PointFieldClient
|
||||
| RadioFieldClient
|
||||
|
||||
@@ -212,25 +212,47 @@ export const promise = async ({
|
||||
}
|
||||
|
||||
case 'group': {
|
||||
await traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
parentIndexPath: '',
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc[field.name] as JsonObject,
|
||||
req,
|
||||
siblingData: (siblingData?.[field.name] as JsonObject) || {},
|
||||
siblingDoc: siblingDoc[field.name] as JsonObject,
|
||||
})
|
||||
if (fieldAffectsData(field)) {
|
||||
await traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
parentIndexPath: '',
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc[field.name] as JsonObject,
|
||||
req,
|
||||
siblingData: (siblingData?.[field.name] as JsonObject) || {},
|
||||
siblingDoc: siblingDoc[field.name] as JsonObject,
|
||||
})
|
||||
} else {
|
||||
await traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
parentIndexPath: indexPath,
|
||||
parentIsLocalized,
|
||||
parentPath,
|
||||
parentSchemaPath: schemaPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: { ...previousSiblingDoc },
|
||||
req,
|
||||
siblingData: siblingData || {},
|
||||
siblingDoc: { ...siblingDoc },
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ export const promise = async ({
|
||||
case 'group': {
|
||||
// Fill groups with empty objects so fields with hooks within groups can populate
|
||||
// themselves virtually as necessary
|
||||
if (typeof siblingDoc[field.name] === 'undefined') {
|
||||
if (fieldAffectsData(field) && typeof siblingDoc[field.name] === 'undefined') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
|
||||
@@ -307,7 +307,11 @@ export const promise = async ({
|
||||
}
|
||||
}
|
||||
|
||||
if ('virtual' in field && typeof field.virtual === 'string') {
|
||||
if (
|
||||
'virtual' in field &&
|
||||
typeof field.virtual === 'string' &&
|
||||
(!field.hidden || showHiddenFields)
|
||||
) {
|
||||
populationPromises.push(
|
||||
virtualFieldPopulationPromise({
|
||||
name: field.name,
|
||||
@@ -609,45 +613,78 @@ export const promise = async ({
|
||||
}
|
||||
|
||||
case 'group': {
|
||||
let groupDoc = siblingDoc[field.name] as JsonObject
|
||||
if (fieldAffectsData(field)) {
|
||||
let groupDoc = siblingDoc[field.name] as JsonObject
|
||||
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
groupDoc = {}
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
groupDoc = {}
|
||||
}
|
||||
|
||||
const groupSelect = select?.[field.name]
|
||||
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: field.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
select: typeof groupSelect === 'object' ? groupSelect : undefined,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
siblingDoc: groupDoc,
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
} else {
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: field.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentIndexPath: indexPath,
|
||||
parentIsLocalized,
|
||||
parentPath,
|
||||
parentSchemaPath: schemaPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
select,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
}
|
||||
|
||||
const groupSelect = select?.[field.name]
|
||||
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: field.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
select: typeof groupSelect === 'object' ? groupSelect : undefined,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
siblingDoc: groupDoc,
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { JsonObject, Operation, PayloadRequest } from '../../../types/index
|
||||
import { ValidationError } from '../../../errors/index.js'
|
||||
import { deepCopyObjectSimple } from '../../../utilities/deepCopyObject.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
export type Args<T extends JsonObject> = {
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
@@ -17,6 +18,7 @@ export type Args<T extends JsonObject> = {
|
||||
global: null | SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
operation: Operation
|
||||
overrideAccess?: boolean
|
||||
req: PayloadRequest
|
||||
skipValidation?: boolean
|
||||
}
|
||||
@@ -39,6 +41,7 @@ export const beforeChange = async <T extends JsonObject>({
|
||||
docWithLocales,
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
req,
|
||||
skipValidation,
|
||||
}: Args<T>): Promise<T> => {
|
||||
@@ -59,6 +62,7 @@ export const beforeChange = async <T extends JsonObject>({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentIsLocalized: false,
|
||||
parentPath: '',
|
||||
|
||||
@@ -45,6 +45,7 @@ type Args = {
|
||||
id?: number | string
|
||||
mergeLocaleActions: (() => Promise<void> | void)[]
|
||||
operation: Operation
|
||||
overrideAccess: boolean
|
||||
parentIndexPath: string
|
||||
parentIsLocalized: boolean
|
||||
parentPath: string
|
||||
@@ -80,6 +81,7 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath,
|
||||
parentIsLocalized,
|
||||
parentPath,
|
||||
@@ -176,6 +178,7 @@ export const promise = async ({
|
||||
object,
|
||||
object
|
||||
>
|
||||
|
||||
const validationResult = await validateFn(valueToValidate as never, {
|
||||
...field,
|
||||
id,
|
||||
@@ -186,6 +189,7 @@ export const promise = async ({
|
||||
// @ts-expect-error
|
||||
jsonError,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: pathSegments,
|
||||
preferences: { fields: {} },
|
||||
previousValue: siblingDoc[field.name],
|
||||
@@ -261,6 +265,7 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
parentPath: path + '.' + rowIndex,
|
||||
@@ -326,6 +331,7 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
parentPath: path + '.' + rowIndex,
|
||||
@@ -368,6 +374,7 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath: indexPath,
|
||||
parentIsLocalized,
|
||||
parentPath,
|
||||
@@ -383,17 +390,42 @@ export const promise = async ({
|
||||
}
|
||||
|
||||
case 'group': {
|
||||
if (typeof siblingData[field.name] !== 'object') {
|
||||
siblingData[field.name] = {}
|
||||
let groupSiblingData = siblingData
|
||||
let groupSiblingDoc = siblingDoc
|
||||
let groupSiblingDocWithLocales = siblingDocWithLocales
|
||||
|
||||
const isNamedGroup = fieldAffectsData(field)
|
||||
|
||||
if (isNamedGroup) {
|
||||
if (typeof siblingData[field.name] !== 'object') {
|
||||
siblingData[field.name] = {}
|
||||
}
|
||||
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
|
||||
if (typeof siblingDocWithLocales[field.name] !== 'object') {
|
||||
siblingDocWithLocales[field.name] = {}
|
||||
}
|
||||
if (typeof siblingData[field.name] !== 'object') {
|
||||
siblingData[field.name] = {}
|
||||
}
|
||||
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
|
||||
if (typeof siblingDocWithLocales[field.name] !== 'object') {
|
||||
siblingDocWithLocales[field.name] = {}
|
||||
}
|
||||
|
||||
groupSiblingData = siblingData[field.name] as JsonObject
|
||||
groupSiblingDoc = siblingDoc[field.name] as JsonObject
|
||||
groupSiblingDocWithLocales = siblingDocWithLocales[field.name] as JsonObject
|
||||
}
|
||||
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
|
||||
if (typeof siblingDocWithLocales[field.name] !== 'object') {
|
||||
siblingDocWithLocales[field.name] = {}
|
||||
}
|
||||
const fallbackLabel = field?.label || (isNamedGroup ? field.name : field?.type)
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
@@ -407,22 +439,20 @@ export const promise = async ({
|
||||
fieldLabelPath:
|
||||
field?.label === false
|
||||
? fieldLabelPath
|
||||
: buildFieldLabel(
|
||||
fieldLabelPath,
|
||||
getTranslatedLabel(field?.label || field?.name, req.i18n),
|
||||
),
|
||||
: buildFieldLabel(fieldLabelPath, getTranslatedLabel(fallbackLabel, req.i18n)),
|
||||
fields: field.fields,
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
parentIndexPath: '',
|
||||
overrideAccess,
|
||||
parentIndexPath: isNamedGroup ? '' : indexPath,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
parentPath: path,
|
||||
parentPath: isNamedGroup ? path : parentPath,
|
||||
parentSchemaPath: schemaPath,
|
||||
req,
|
||||
siblingData: siblingData[field.name] as JsonObject,
|
||||
siblingDoc: siblingDoc[field.name] as JsonObject,
|
||||
siblingDocWithLocales: siblingDocWithLocales[field.name] as JsonObject,
|
||||
siblingData: groupSiblingData,
|
||||
siblingDoc: groupSiblingDoc,
|
||||
siblingDocWithLocales: groupSiblingDocWithLocales,
|
||||
skipValidation: skipValidationFromHere,
|
||||
})
|
||||
|
||||
@@ -480,6 +510,7 @@ export const promise = async ({
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
parentIsLocalized,
|
||||
path: pathSegments,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
@@ -546,6 +577,7 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath: isNamedTab ? '' : indexPath,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
parentPath: isNamedTab ? path : parentPath,
|
||||
@@ -578,6 +610,7 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath: indexPath,
|
||||
parentIsLocalized,
|
||||
parentPath: path,
|
||||
|
||||
@@ -36,6 +36,7 @@ type Args = {
|
||||
id?: number | string
|
||||
mergeLocaleActions: (() => Promise<void> | void)[]
|
||||
operation: Operation
|
||||
overrideAccess: boolean
|
||||
parentIndexPath: string
|
||||
/**
|
||||
* @todo make required in v4.0
|
||||
@@ -78,6 +79,7 @@ export const traverseFields = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath,
|
||||
parentIsLocalized,
|
||||
parentPath,
|
||||
@@ -107,6 +109,7 @@ export const traverseFields = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath,
|
||||
parentIsLocalized,
|
||||
parentPath,
|
||||
|
||||
@@ -375,9 +375,10 @@ export const promise = async <T>({
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Finally, we traverse fields which do not affect data here
|
||||
// Finally, we traverse fields which do not affect data here - collapsibles, rows, unnamed groups
|
||||
switch (field.type) {
|
||||
case 'collapsible':
|
||||
case 'group':
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
id,
|
||||
|
||||
@@ -447,16 +447,23 @@ export const promise = async <T>({
|
||||
}
|
||||
|
||||
case 'group': {
|
||||
if (typeof siblingData[field.name] !== 'object') {
|
||||
siblingData[field.name] = {}
|
||||
}
|
||||
let groupSiblingData = siblingData
|
||||
let groupSiblingDoc = siblingDoc
|
||||
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
const isNamedGroup = fieldAffectsData(field)
|
||||
|
||||
const groupData = siblingData[field.name] as Record<string, unknown>
|
||||
const groupDoc = siblingDoc[field.name] as Record<string, unknown>
|
||||
if (isNamedGroup) {
|
||||
if (typeof siblingData[field.name] !== 'object') {
|
||||
siblingData[field.name] = {}
|
||||
}
|
||||
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
|
||||
groupSiblingData = siblingData[field.name] as Record<string, unknown>
|
||||
groupSiblingDoc = siblingDoc[field.name] as Record<string, unknown>
|
||||
}
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
@@ -469,13 +476,13 @@ export const promise = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentIndexPath: isNamedGroup ? '' : indexPath,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
parentPath: path,
|
||||
parentPath: isNamedGroup ? path : parentPath,
|
||||
parentSchemaPath: schemaPath,
|
||||
req,
|
||||
siblingData: groupData as JsonObject,
|
||||
siblingDoc: groupDoc as JsonObject,
|
||||
siblingData: groupSiblingData,
|
||||
siblingDoc: groupSiblingDoc,
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user