Compare commits
12 Commits
chore/ci-e
...
fix/circul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
504ecd1c43 | ||
|
|
c98d392c73 | ||
|
|
949486607b | ||
|
|
6bc743b9a7 | ||
|
|
54269f2dba | ||
|
|
49f91a45f3 | ||
|
|
99623838cd | ||
|
|
a94d98dd92 | ||
|
|
d003f2b800 | ||
|
|
ba643f8b7d | ||
|
|
5e1c7b511d | ||
|
|
d948f4b819 |
17
.madgerc
Normal file
17
.madgerc
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"detectiveOptions": {
|
||||
"ts": {
|
||||
"skipTypeImports": true
|
||||
}
|
||||
},
|
||||
"dependencyFilter": false,
|
||||
"includeNpm": true,
|
||||
"tsConfig": "/Users/alessio/Documents/GitHub/payload20/tsconfig.json",
|
||||
"fileExtensions": [
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
"jsx"
|
||||
],
|
||||
"baseDir": "/Users/alessio/Documents/GitHub/payload20"
|
||||
}
|
||||
12
package.json
12
package.json
@@ -4,6 +4,18 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"analyze-circular-dependencies": "madge test/dev.ts --circular --warning",
|
||||
"analyze-circular-dependencies-fields": "madge test/fields/config.ts --circular --warning",
|
||||
"analyze-circular-dependencies-lexical": "madge packages/richtext-lexical/src/index.ts --circular --warning",
|
||||
"analyze-circular-dependencies-lexical-client": "madge packages/richtext-lexical/src/exports/client/index.ts --circular --warning",
|
||||
"analyze-circular-dependencies-lexical-migrate": "madge packages/richtext-lexical/src/exports/server/migrate.ts --circular --warning",
|
||||
"analyze-circular-dependencies-mongodb": "madge packages/db-mongodb/src/index.ts --circular --warning",
|
||||
"analyze-circular-dependencies-postgres": "madge packages/db-postgres/src/index.ts --circular --warning",
|
||||
"analyze-circular-dependencies-drizzle": "madge packages/drizzle/src/index.ts --circular --warning",
|
||||
"analyze-circular-dependencies-drizzle-postgres": "madge packages/drizzle/src/exports/postgres.ts --circular --warning",
|
||||
"analyze-circular-dependencies-graphql": "madge packages/grapqhl/src/index.ts --circular --warning",
|
||||
"analyze-circular-dependencies-fieldSchemasToFormState": "madge packages/ui/src/forms/fieldSchemasToFormState/index.tsx --circular --warning",
|
||||
"analyze-circular-dependencies-buildFieldSchemaMap": "madge packages/ui/src/utilities/buildFieldSchemaMap/index.ts --circular --warning",
|
||||
"bf": "pnpm run build:force",
|
||||
"build": "pnpm run build:core",
|
||||
"build:all": "turbo build",
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import type { Field, Payload, Where } from 'payload'
|
||||
|
||||
import { parseParams } from './parseParams.js'
|
||||
|
||||
export async function buildAndOrConditions({
|
||||
collectionSlug,
|
||||
fields,
|
||||
globalSlug,
|
||||
locale,
|
||||
payload,
|
||||
where,
|
||||
}: {
|
||||
collectionSlug?: string
|
||||
fields: Field[]
|
||||
globalSlug?: string
|
||||
locale?: string
|
||||
payload: Payload
|
||||
where: Where[]
|
||||
}): Promise<Record<string, unknown>[]> {
|
||||
const completedConditions = []
|
||||
// Loop over all AND / OR operations and add them to the AND / OR query param
|
||||
// Operations should come through as an array
|
||||
|
||||
for (const condition of where) {
|
||||
// If the operation is properly formatted as an object
|
||||
if (typeof condition === 'object') {
|
||||
const result = await parseParams({
|
||||
collectionSlug,
|
||||
fields,
|
||||
globalSlug,
|
||||
locale,
|
||||
payload,
|
||||
where: condition,
|
||||
})
|
||||
if (Object.keys(result).length > 0) {
|
||||
completedConditions.push(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
return completedConditions
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import type { Field, Operator, Payload, Where } from 'payload'
|
||||
import { deepMergeWithCombinedArrays } from 'payload'
|
||||
import { validOperators } from 'payload/shared'
|
||||
|
||||
import { buildAndOrConditions } from './buildAndOrConditions.js'
|
||||
import { buildSearchParam } from './buildSearchParams.js'
|
||||
|
||||
export async function parseParams({
|
||||
@@ -85,3 +84,41 @@ export async function parseParams({
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export async function buildAndOrConditions({
|
||||
collectionSlug,
|
||||
fields,
|
||||
globalSlug,
|
||||
locale,
|
||||
payload,
|
||||
where,
|
||||
}: {
|
||||
collectionSlug?: string
|
||||
fields: Field[]
|
||||
globalSlug?: string
|
||||
locale?: string
|
||||
payload: Payload
|
||||
where: Where[]
|
||||
}): Promise<Record<string, unknown>[]> {
|
||||
const completedConditions = []
|
||||
// Loop over all AND / OR operations and add them to the AND / OR query param
|
||||
// Operations should come through as an array
|
||||
|
||||
for (const condition of where) {
|
||||
// If the operation is properly formatted as an object
|
||||
if (typeof condition === 'object') {
|
||||
const result = await parseParams({
|
||||
collectionSlug,
|
||||
fields,
|
||||
globalSlug,
|
||||
locale,
|
||||
payload,
|
||||
where: condition,
|
||||
})
|
||||
if (Object.keys(result).length > 0) {
|
||||
completedConditions.push(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
return completedConditions
|
||||
}
|
||||
|
||||
@@ -5,19 +5,27 @@ import type {
|
||||
PgColumnBuilder,
|
||||
PgTableWithColumns,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import type { Field, SanitizedJoins } from 'payload'
|
||||
import type { Field, TabAsField } from 'payload'
|
||||
|
||||
import { relations } from 'drizzle-orm'
|
||||
import {
|
||||
boolean,
|
||||
foreignKey,
|
||||
index,
|
||||
integer,
|
||||
jsonb,
|
||||
numeric,
|
||||
PgNumericBuilder,
|
||||
PgUUIDBuilder,
|
||||
PgVarcharBuilder,
|
||||
serial,
|
||||
text,
|
||||
timestamp,
|
||||
unique,
|
||||
varchar,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import { InvalidConfiguration } from 'payload'
|
||||
import { fieldAffectsData, fieldIsVirtual, optionIsObject } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type {
|
||||
@@ -31,10 +39,14 @@ import type {
|
||||
|
||||
import { createTableName } from '../../createTableName.js'
|
||||
import { buildIndexName } from '../../utilities/buildIndexName.js'
|
||||
import { hasLocalesTable } from '../../utilities/hasLocalesTable.js'
|
||||
import { validateExistingBlockIsIdentical } from '../../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { createIndex } from './createIndex.js'
|
||||
import { geometryColumn } from './geometryColumn.js'
|
||||
import { idToUUID } from './idToUUID.js'
|
||||
import { parentIDColumnMap } from './parentIDColumnMap.js'
|
||||
import { setColumnID } from './setColumnID.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
import { withDefault } from './withDefault.js'
|
||||
|
||||
type Args = {
|
||||
adapter: BasePostgresAdapter
|
||||
@@ -520,3 +532,925 @@ export const buildTable = ({
|
||||
relationsToBuild,
|
||||
}
|
||||
}
|
||||
|
||||
type TraverseFieldsArgs = {
|
||||
adapter: BasePostgresAdapter
|
||||
columnPrefix?: string
|
||||
columns: Record<string, PgColumnBuilder>
|
||||
disableNotNull: boolean
|
||||
disableRelsTableUnique?: boolean
|
||||
disableUnique?: boolean
|
||||
fieldPrefix?: string
|
||||
fields: (Field | TabAsField)[]
|
||||
forceLocalized?: boolean
|
||||
indexes: Record<string, (cols: GenericColumns) => IndexBuilder>
|
||||
localesColumns: Record<string, PgColumnBuilder>
|
||||
localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder>
|
||||
newTableName: string
|
||||
parentTableName: string
|
||||
relationships: Set<string>
|
||||
relationsToBuild: RelationMap
|
||||
rootRelationsToBuild?: RelationMap
|
||||
rootTableIDColType: string
|
||||
rootTableName: string
|
||||
uniqueRelationships: Set<string>
|
||||
versions: boolean
|
||||
/**
|
||||
* Tracks whether or not this table is built
|
||||
* from the result of a localized array or block field at some point
|
||||
*/
|
||||
withinLocalizedArrayOrBlock?: boolean
|
||||
}
|
||||
|
||||
type TraverseFieldsResult = {
|
||||
hasLocalizedField: boolean
|
||||
hasLocalizedManyNumberField: boolean
|
||||
hasLocalizedManyTextField: boolean
|
||||
hasLocalizedRelationshipField: boolean
|
||||
hasManyNumberField: 'index' | boolean
|
||||
hasManyTextField: 'index' | boolean
|
||||
}
|
||||
|
||||
export const traverseFields = ({
|
||||
adapter,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull,
|
||||
disableRelsTableUnique,
|
||||
disableUnique = false,
|
||||
fieldPrefix,
|
||||
fields,
|
||||
forceLocalized,
|
||||
indexes,
|
||||
localesColumns,
|
||||
localesIndexes,
|
||||
newTableName,
|
||||
parentTableName,
|
||||
relationships,
|
||||
relationsToBuild,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
uniqueRelationships,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
}: TraverseFieldsArgs): TraverseFieldsResult => {
|
||||
const throwValidationError = true
|
||||
let hasLocalizedField = false
|
||||
let hasLocalizedRelationshipField = false
|
||||
let hasManyTextField: 'index' | boolean = false
|
||||
let hasLocalizedManyTextField = false
|
||||
let hasManyNumberField: 'index' | boolean = false
|
||||
let hasLocalizedManyNumberField = false
|
||||
|
||||
let parentIDColType: IDType = 'integer'
|
||||
if (columns.id instanceof PgUUIDBuilder) {
|
||||
parentIDColType = 'uuid'
|
||||
}
|
||||
if (columns.id instanceof PgNumericBuilder) {
|
||||
parentIDColType = 'numeric'
|
||||
}
|
||||
if (columns.id instanceof PgVarcharBuilder) {
|
||||
parentIDColType = 'varchar'
|
||||
}
|
||||
|
||||
fields.forEach((field) => {
|
||||
if ('name' in field && field.name === 'id') {
|
||||
return
|
||||
}
|
||||
if (fieldIsVirtual(field)) {
|
||||
return
|
||||
}
|
||||
|
||||
let columnName: string
|
||||
let fieldName: string
|
||||
|
||||
let targetTable = columns
|
||||
let targetIndexes = indexes
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
columnName = `${columnPrefix || ''}${field.name[0] === '_' ? '_' : ''}${toSnakeCase(
|
||||
field.name,
|
||||
)}`
|
||||
fieldName = `${fieldPrefix?.replace('.', '_') || ''}${field.name}`
|
||||
|
||||
// If field is localized,
|
||||
// add the column to the locale table instead of main table
|
||||
if (
|
||||
adapter.payload.config.localization &&
|
||||
(field.localized || forceLocalized) &&
|
||||
field.type !== 'array' &&
|
||||
field.type !== 'blocks' &&
|
||||
(('hasMany' in field && field.hasMany !== true) || !('hasMany' in field))
|
||||
) {
|
||||
hasLocalizedField = true
|
||||
targetTable = localesColumns
|
||||
targetIndexes = localesIndexes
|
||||
}
|
||||
|
||||
if (
|
||||
(field.unique || field.index || ['relationship', 'upload'].includes(field.type)) &&
|
||||
!['array', 'blocks', 'group'].includes(field.type) &&
|
||||
!('hasMany' in field && field.hasMany === true) &&
|
||||
!('relationTo' in field && Array.isArray(field.relationTo))
|
||||
) {
|
||||
const unique = disableUnique !== true && field.unique
|
||||
if (unique) {
|
||||
const constraintValue = `${fieldPrefix || ''}${field.name}`
|
||||
if (!adapter.fieldConstraints?.[rootTableName]) {
|
||||
adapter.fieldConstraints[rootTableName] = {}
|
||||
}
|
||||
adapter.fieldConstraints[rootTableName][`${columnName}_idx`] = constraintValue
|
||||
}
|
||||
|
||||
const indexName = buildIndexName({ name: `${newTableName}_${columnName}`, adapter })
|
||||
|
||||
targetIndexes[indexName] = createIndex({
|
||||
name: field.localized ? [fieldName, '_locale'] : fieldName,
|
||||
indexName,
|
||||
unique,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
const arrayTableName = createTableName({
|
||||
adapter,
|
||||
config: field,
|
||||
parentTableName: newTableName,
|
||||
prefix: `${newTableName}_`,
|
||||
throwValidationError,
|
||||
versionsCustomName: versions,
|
||||
})
|
||||
|
||||
const baseColumns: Record<string, PgColumnBuilder> = {
|
||||
_order: integer('_order').notNull(),
|
||||
_parentID: parentIDColumnMap[parentIDColType]('_parent_id').notNull(),
|
||||
}
|
||||
|
||||
const baseExtraConfig: BaseExtraConfig = {
|
||||
_orderIdx: (cols) => index(`${arrayTableName}_order_idx`).on(cols._order),
|
||||
_parentIDFk: (cols) =>
|
||||
foreignKey({
|
||||
name: `${arrayTableName}_parent_id_fk`,
|
||||
columns: [cols['_parentID']],
|
||||
foreignColumns: [adapter.tables[parentTableName].id],
|
||||
}).onDelete('cascade'),
|
||||
_parentIDIdx: (cols) => index(`${arrayTableName}_parent_id_idx`).on(cols._parentID),
|
||||
}
|
||||
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
||||
baseExtraConfig._localeIdx = (cols) =>
|
||||
index(`${arrayTableName}_locale_idx`).on(cols._locale)
|
||||
}
|
||||
|
||||
const {
|
||||
hasLocalizedManyNumberField: subHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: subHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: subHasLocalizedRelationshipField,
|
||||
hasManyNumberField: subHasManyNumberField,
|
||||
hasManyTextField: subHasManyTextField,
|
||||
relationsToBuild: subRelationsToBuild,
|
||||
} = buildTable({
|
||||
adapter,
|
||||
baseColumns,
|
||||
baseExtraConfig,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableRelsTableUnique: true,
|
||||
disableUnique,
|
||||
fields: disableUnique ? idToUUID(field.fields) : field.fields,
|
||||
rootRelationships: relationships,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
rootUniqueRelationships: uniqueRelationships,
|
||||
tableName: arrayTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock: isLocalized,
|
||||
})
|
||||
|
||||
if (subHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = subHasLocalizedManyNumberField
|
||||
}
|
||||
|
||||
if (subHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = subHasLocalizedRelationshipField
|
||||
}
|
||||
|
||||
if (subHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = subHasLocalizedManyTextField
|
||||
}
|
||||
|
||||
if (subHasManyTextField) {
|
||||
if (!hasManyTextField || subHasManyTextField === 'index') {
|
||||
hasManyTextField = subHasManyTextField
|
||||
}
|
||||
}
|
||||
if (subHasManyNumberField) {
|
||||
if (!hasManyNumberField || subHasManyNumberField === 'index') {
|
||||
hasManyNumberField = subHasManyNumberField
|
||||
}
|
||||
}
|
||||
|
||||
relationsToBuild.set(fieldName, {
|
||||
type: 'many',
|
||||
// arrays have their own localized table, independent of the base table.
|
||||
localized: false,
|
||||
target: arrayTableName,
|
||||
})
|
||||
|
||||
adapter.relations[`relations_${arrayTableName}`] = relations(
|
||||
adapter.tables[arrayTableName],
|
||||
({ many, one }) => {
|
||||
const result: Record<string, Relation<string>> = {
|
||||
_parentID: one(adapter.tables[parentTableName], {
|
||||
fields: [adapter.tables[arrayTableName]._parentID],
|
||||
references: [adapter.tables[parentTableName].id],
|
||||
relationName: fieldName,
|
||||
}),
|
||||
}
|
||||
|
||||
if (hasLocalesTable(field.fields)) {
|
||||
result._locales = many(adapter.tables[`${arrayTableName}${adapter.localesSuffix}`], {
|
||||
relationName: '_locales',
|
||||
})
|
||||
}
|
||||
|
||||
subRelationsToBuild.forEach(({ type, localized, target }, key) => {
|
||||
if (type === 'one') {
|
||||
const arrayWithLocalized = localized
|
||||
? `${arrayTableName}${adapter.localesSuffix}`
|
||||
: arrayTableName
|
||||
result[key] = one(adapter.tables[target], {
|
||||
fields: [adapter.tables[arrayWithLocalized][key]],
|
||||
references: [adapter.tables[target].id],
|
||||
relationName: key,
|
||||
})
|
||||
}
|
||||
if (type === 'many') {
|
||||
result[key] = many(adapter.tables[target], { relationName: key })
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
},
|
||||
)
|
||||
|
||||
break
|
||||
}
|
||||
case 'blocks': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
field.blocks.forEach((block) => {
|
||||
const blockTableName = createTableName({
|
||||
adapter,
|
||||
config: block,
|
||||
parentTableName: rootTableName,
|
||||
prefix: `${rootTableName}_blocks_`,
|
||||
throwValidationError,
|
||||
versionsCustomName: versions,
|
||||
})
|
||||
if (!adapter.tables[blockTableName]) {
|
||||
const baseColumns: Record<string, PgColumnBuilder> = {
|
||||
_order: integer('_order').notNull(),
|
||||
_parentID: parentIDColumnMap[rootTableIDColType]('_parent_id').notNull(),
|
||||
_path: text('_path').notNull(),
|
||||
}
|
||||
|
||||
const baseExtraConfig: BaseExtraConfig = {
|
||||
_orderIdx: (cols) => index(`${blockTableName}_order_idx`).on(cols._order),
|
||||
_parentIdFk: (cols) =>
|
||||
foreignKey({
|
||||
name: `${blockTableName}_parent_id_fk`,
|
||||
columns: [cols._parentID],
|
||||
foreignColumns: [adapter.tables[rootTableName].id],
|
||||
}).onDelete('cascade'),
|
||||
_parentIDIdx: (cols) => index(`${blockTableName}_parent_id_idx`).on(cols._parentID),
|
||||
_pathIdx: (cols) => index(`${blockTableName}_path_idx`).on(cols._path),
|
||||
}
|
||||
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
||||
baseExtraConfig._localeIdx = (cols) =>
|
||||
index(`${blockTableName}_locale_idx`).on(cols._locale)
|
||||
}
|
||||
|
||||
const {
|
||||
hasLocalizedManyNumberField: subHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: subHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: subHasLocalizedRelationshipField,
|
||||
hasManyNumberField: subHasManyNumberField,
|
||||
hasManyTextField: subHasManyTextField,
|
||||
relationsToBuild: subRelationsToBuild,
|
||||
} = buildTable({
|
||||
adapter,
|
||||
baseColumns,
|
||||
baseExtraConfig,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableRelsTableUnique: true,
|
||||
disableUnique,
|
||||
fields: disableUnique ? idToUUID(block.fields) : block.fields,
|
||||
rootRelationships: relationships,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
rootUniqueRelationships: uniqueRelationships,
|
||||
tableName: blockTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock: isLocalized,
|
||||
})
|
||||
|
||||
if (subHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = subHasLocalizedManyNumberField
|
||||
}
|
||||
|
||||
if (subHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = subHasLocalizedRelationshipField
|
||||
}
|
||||
|
||||
if (subHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = subHasLocalizedManyTextField
|
||||
}
|
||||
|
||||
if (subHasManyTextField) {
|
||||
if (!hasManyTextField || subHasManyTextField === 'index') {
|
||||
hasManyTextField = subHasManyTextField
|
||||
}
|
||||
}
|
||||
|
||||
if (subHasManyNumberField) {
|
||||
if (!hasManyNumberField || subHasManyNumberField === 'index') {
|
||||
hasManyNumberField = subHasManyNumberField
|
||||
}
|
||||
}
|
||||
|
||||
adapter.relations[`relations_${blockTableName}`] = relations(
|
||||
adapter.tables[blockTableName],
|
||||
({ many, one }) => {
|
||||
const result: Record<string, Relation<string>> = {
|
||||
_parentID: one(adapter.tables[rootTableName], {
|
||||
fields: [adapter.tables[blockTableName]._parentID],
|
||||
references: [adapter.tables[rootTableName].id],
|
||||
relationName: `_blocks_${block.slug}`,
|
||||
}),
|
||||
}
|
||||
|
||||
if (hasLocalesTable(block.fields)) {
|
||||
result._locales = many(
|
||||
adapter.tables[`${blockTableName}${adapter.localesSuffix}`],
|
||||
{ relationName: '_locales' },
|
||||
)
|
||||
}
|
||||
|
||||
subRelationsToBuild.forEach(({ type, localized, target }, key) => {
|
||||
if (type === 'one') {
|
||||
const blockWithLocalized = localized
|
||||
? `${blockTableName}${adapter.localesSuffix}`
|
||||
: blockTableName
|
||||
result[key] = one(adapter.tables[target], {
|
||||
fields: [adapter.tables[blockWithLocalized][key]],
|
||||
references: [adapter.tables[target].id],
|
||||
relationName: key,
|
||||
})
|
||||
}
|
||||
if (type === 'many') {
|
||||
result[key] = many(adapter.tables[target], { relationName: key })
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
},
|
||||
)
|
||||
} else if (process.env.NODE_ENV !== 'production' && !versions) {
|
||||
validateExistingBlockIsIdentical({
|
||||
block,
|
||||
localized: field.localized,
|
||||
rootTableName,
|
||||
table: adapter.tables[blockTableName],
|
||||
tableLocales: adapter.tables[`${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}`, {
|
||||
type: 'many',
|
||||
// blocks are not localized on the parent table
|
||||
localized: false,
|
||||
target: blockTableName,
|
||||
})
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
case 'checkbox': {
|
||||
targetTable[fieldName] = withDefault(boolean(columnName), field)
|
||||
break
|
||||
}
|
||||
case 'code':
|
||||
|
||||
case 'email':
|
||||
|
||||
case 'textarea': {
|
||||
targetTable[fieldName] = withDefault(varchar(columnName), field)
|
||||
break
|
||||
}
|
||||
case 'collapsible':
|
||||
|
||||
case 'row': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
const {
|
||||
hasLocalizedField: rowHasLocalizedField,
|
||||
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: rowHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: rowHasLocalizedRelationshipField,
|
||||
hasManyNumberField: rowHasManyNumberField,
|
||||
hasManyTextField: rowHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fieldPrefix,
|
||||
fields: field.fields,
|
||||
forceLocalized,
|
||||
indexes,
|
||||
localesColumns,
|
||||
localesIndexes,
|
||||
newTableName,
|
||||
parentTableName,
|
||||
relationships,
|
||||
relationsToBuild,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
uniqueRelationships,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
|
||||
if (rowHasLocalizedField) {
|
||||
hasLocalizedField = true
|
||||
}
|
||||
if (rowHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = true
|
||||
}
|
||||
if (rowHasManyTextField) {
|
||||
hasManyTextField = true
|
||||
}
|
||||
if (rowHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
if (rowHasManyNumberField) {
|
||||
hasManyNumberField = true
|
||||
}
|
||||
if (rowHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = true
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'date': {
|
||||
targetTable[fieldName] = withDefault(
|
||||
timestamp(columnName, {
|
||||
mode: 'string',
|
||||
precision: 3,
|
||||
withTimezone: true,
|
||||
}),
|
||||
field,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case 'group':
|
||||
case 'tab': {
|
||||
if (!('name' in field)) {
|
||||
const {
|
||||
hasLocalizedField: groupHasLocalizedField,
|
||||
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: groupHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: groupHasLocalizedRelationshipField,
|
||||
hasManyNumberField: groupHasManyNumberField,
|
||||
hasManyTextField: groupHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull,
|
||||
disableUnique,
|
||||
fieldPrefix,
|
||||
fields: field.fields,
|
||||
forceLocalized,
|
||||
indexes,
|
||||
localesColumns,
|
||||
localesIndexes,
|
||||
newTableName,
|
||||
parentTableName,
|
||||
relationships,
|
||||
relationsToBuild,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
uniqueRelationships,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
|
||||
if (groupHasLocalizedField) {
|
||||
hasLocalizedField = true
|
||||
}
|
||||
if (groupHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = true
|
||||
}
|
||||
if (groupHasManyTextField) {
|
||||
hasManyTextField = true
|
||||
}
|
||||
if (groupHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
if (groupHasManyNumberField) {
|
||||
hasManyNumberField = true
|
||||
}
|
||||
if (groupHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = true
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
const {
|
||||
hasLocalizedField: groupHasLocalizedField,
|
||||
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: groupHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: groupHasLocalizedRelationshipField,
|
||||
hasManyNumberField: groupHasManyNumberField,
|
||||
hasManyTextField: groupHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
columnPrefix: `${columnName}_`,
|
||||
columns,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fieldPrefix: `${fieldName}.`,
|
||||
fields: field.fields,
|
||||
forceLocalized: field.localized,
|
||||
indexes,
|
||||
localesColumns,
|
||||
localesIndexes,
|
||||
newTableName: `${parentTableName}_${columnName}`,
|
||||
parentTableName,
|
||||
relationships,
|
||||
relationsToBuild,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
uniqueRelationships,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized,
|
||||
})
|
||||
|
||||
if (groupHasLocalizedField) {
|
||||
hasLocalizedField = true
|
||||
}
|
||||
if (groupHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = true
|
||||
}
|
||||
if (groupHasManyTextField) {
|
||||
hasManyTextField = true
|
||||
}
|
||||
if (groupHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
if (groupHasManyNumberField) {
|
||||
hasManyNumberField = true
|
||||
}
|
||||
if (groupHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = true
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'json':
|
||||
|
||||
case 'richText': {
|
||||
targetTable[fieldName] = withDefault(jsonb(columnName), field)
|
||||
break
|
||||
}
|
||||
|
||||
case 'number': {
|
||||
if (field.hasMany) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
hasLocalizedManyNumberField = true
|
||||
}
|
||||
|
||||
if (field.index) {
|
||||
hasManyNumberField = 'index'
|
||||
} else if (!hasManyNumberField) {
|
||||
hasManyNumberField = true
|
||||
}
|
||||
|
||||
if (field.unique) {
|
||||
throw new InvalidConfiguration(
|
||||
'Unique is not supported in Postgres for hasMany number fields.',
|
||||
)
|
||||
}
|
||||
} else {
|
||||
targetTable[fieldName] = withDefault(numeric(columnName), field)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'point': {
|
||||
targetTable[fieldName] = withDefault(geometryColumn(columnName), field)
|
||||
if (!adapter.extensions.postgis) {
|
||||
adapter.extensions.postgis = true
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'radio':
|
||||
|
||||
case 'select': {
|
||||
const enumName = createTableName({
|
||||
adapter,
|
||||
config: field,
|
||||
parentTableName: newTableName,
|
||||
prefix: `enum_${newTableName}_`,
|
||||
target: 'enumName',
|
||||
throwValidationError,
|
||||
})
|
||||
|
||||
adapter.enums[enumName] = adapter.pgSchema.enum(
|
||||
enumName,
|
||||
field.options.map((option) => {
|
||||
if (optionIsObject(option)) {
|
||||
return option.value
|
||||
}
|
||||
|
||||
return option
|
||||
}) as [string, ...string[]],
|
||||
)
|
||||
|
||||
if (field.type === 'select' && field.hasMany) {
|
||||
const selectTableName = createTableName({
|
||||
adapter,
|
||||
config: field,
|
||||
parentTableName: newTableName,
|
||||
prefix: `${newTableName}_`,
|
||||
throwValidationError,
|
||||
versionsCustomName: versions,
|
||||
})
|
||||
const baseColumns: Record<string, PgColumnBuilder> = {
|
||||
order: integer('order').notNull(),
|
||||
parent: parentIDColumnMap[parentIDColType]('parent_id').notNull(),
|
||||
value: adapter.enums[enumName]('value'),
|
||||
}
|
||||
|
||||
const baseExtraConfig: BaseExtraConfig = {
|
||||
orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order),
|
||||
parentFk: (cols) =>
|
||||
foreignKey({
|
||||
name: `${selectTableName}_parent_fk`,
|
||||
columns: [cols.parent],
|
||||
foreignColumns: [adapter.tables[parentTableName].id],
|
||||
}).onDelete('cascade'),
|
||||
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
|
||||
}
|
||||
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
baseColumns.locale = adapter.enums.enum__locales('locale').notNull()
|
||||
baseExtraConfig.localeIdx = (cols) =>
|
||||
index(`${selectTableName}_locale_idx`).on(cols.locale)
|
||||
}
|
||||
|
||||
if (field.index) {
|
||||
baseExtraConfig.value = (cols) => index(`${selectTableName}_value_idx`).on(cols.value)
|
||||
}
|
||||
|
||||
buildTable({
|
||||
adapter,
|
||||
baseColumns,
|
||||
baseExtraConfig,
|
||||
disableNotNull,
|
||||
disableUnique,
|
||||
fields: [],
|
||||
rootTableName,
|
||||
tableName: selectTableName,
|
||||
versions,
|
||||
})
|
||||
|
||||
relationsToBuild.set(fieldName, {
|
||||
type: 'many',
|
||||
// selects have their own localized table, independent of the base table.
|
||||
localized: false,
|
||||
target: selectTableName,
|
||||
})
|
||||
|
||||
adapter.relations[`relations_${selectTableName}`] = relations(
|
||||
adapter.tables[selectTableName],
|
||||
({ one }) => ({
|
||||
parent: one(adapter.tables[parentTableName], {
|
||||
fields: [adapter.tables[selectTableName].parent],
|
||||
references: [adapter.tables[parentTableName].id],
|
||||
relationName: fieldName,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
targetTable[fieldName] = withDefault(adapter.enums[enumName](columnName), field)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
case 'upload':
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
field.relationTo.forEach((relation) => {
|
||||
relationships.add(relation)
|
||||
if (field.unique && !disableUnique && !disableRelsTableUnique) {
|
||||
uniqueRelationships.add(relation)
|
||||
}
|
||||
})
|
||||
} else if (field.hasMany) {
|
||||
relationships.add(field.relationTo)
|
||||
if (field.unique && !disableUnique && !disableRelsTableUnique) {
|
||||
uniqueRelationships.add(field.relationTo)
|
||||
}
|
||||
} else {
|
||||
// simple relationships get a column on the targetTable with a foreign key to the relationTo table
|
||||
const relationshipConfig = adapter.payload.collections[field.relationTo].config
|
||||
|
||||
const tableName = adapter.tableNameMap.get(toSnakeCase(field.relationTo))
|
||||
|
||||
// get the id type of the related collection
|
||||
let colType = adapter.idType === 'uuid' ? 'uuid' : 'integer'
|
||||
const relatedCollectionCustomID = relationshipConfig.fields.find(
|
||||
(field) => fieldAffectsData(field) && field.name === 'id',
|
||||
)
|
||||
if (relatedCollectionCustomID?.type === 'number') {
|
||||
colType = 'numeric'
|
||||
}
|
||||
if (relatedCollectionCustomID?.type === 'text') {
|
||||
colType = 'varchar'
|
||||
}
|
||||
|
||||
// make the foreign key column for relationship using the correct id column type
|
||||
targetTable[fieldName] = parentIDColumnMap[colType](`${columnName}_id`).references(
|
||||
() => adapter.tables[tableName].id,
|
||||
{ onDelete: 'set null' },
|
||||
)
|
||||
|
||||
// add relationship to table
|
||||
relationsToBuild.set(fieldName, {
|
||||
type: 'one',
|
||||
localized: adapter.payload.config.localization && (field.localized || forceLocalized),
|
||||
target: tableName,
|
||||
})
|
||||
|
||||
// add notNull when not required
|
||||
if (!disableNotNull && field.required && !field.admin?.condition) {
|
||||
targetTable[fieldName].notNull()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if (
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock
|
||||
) {
|
||||
hasLocalizedRelationshipField = true
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case 'tabs': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
const {
|
||||
hasLocalizedField: tabHasLocalizedField,
|
||||
hasLocalizedManyNumberField: tabHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: tabHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: tabHasLocalizedRelationshipField,
|
||||
hasManyNumberField: tabHasManyNumberField,
|
||||
hasManyTextField: tabHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fieldPrefix,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
forceLocalized,
|
||||
indexes,
|
||||
localesColumns,
|
||||
localesIndexes,
|
||||
newTableName,
|
||||
parentTableName,
|
||||
relationships,
|
||||
relationsToBuild,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
uniqueRelationships,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
|
||||
if (tabHasLocalizedField) {
|
||||
hasLocalizedField = true
|
||||
}
|
||||
if (tabHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = true
|
||||
}
|
||||
if (tabHasManyTextField) {
|
||||
hasManyTextField = true
|
||||
}
|
||||
if (tabHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
if (tabHasManyNumberField) {
|
||||
hasManyNumberField = true
|
||||
}
|
||||
if (tabHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = true
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'text': {
|
||||
if (field.hasMany) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
|
||||
if (field.index) {
|
||||
hasManyTextField = 'index'
|
||||
} else if (!hasManyTextField) {
|
||||
hasManyTextField = true
|
||||
}
|
||||
|
||||
if (field.unique) {
|
||||
throw new InvalidConfiguration(
|
||||
'Unique is not supported in Postgres for hasMany text fields.',
|
||||
)
|
||||
}
|
||||
} else {
|
||||
targetTable[fieldName] = withDefault(varchar(columnName), field)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
const condition = field.admin && field.admin.condition
|
||||
|
||||
if (
|
||||
!disableNotNull &&
|
||||
targetTable[fieldName] &&
|
||||
'required' in field &&
|
||||
field.required &&
|
||||
!condition
|
||||
) {
|
||||
targetTable[fieldName].notNull()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
hasLocalizedField,
|
||||
hasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField,
|
||||
hasManyNumberField,
|
||||
hasManyTextField,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,964 +0,0 @@
|
||||
import type { Relation } from 'drizzle-orm'
|
||||
import type { IndexBuilder, PgColumnBuilder } from 'drizzle-orm/pg-core'
|
||||
import type { Field, SanitizedJoins, TabAsField } from 'payload'
|
||||
|
||||
import { relations } from 'drizzle-orm'
|
||||
import {
|
||||
boolean,
|
||||
foreignKey,
|
||||
geometry,
|
||||
index,
|
||||
integer,
|
||||
jsonb,
|
||||
numeric,
|
||||
PgNumericBuilder,
|
||||
PgUUIDBuilder,
|
||||
PgVarcharBuilder,
|
||||
text,
|
||||
timestamp,
|
||||
varchar,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import { InvalidConfiguration } from 'payload'
|
||||
import { fieldAffectsData, fieldIsVirtual, optionIsObject } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type {
|
||||
BaseExtraConfig,
|
||||
BasePostgresAdapter,
|
||||
GenericColumns,
|
||||
IDType,
|
||||
RelationMap,
|
||||
} from '../types.js'
|
||||
|
||||
import { createTableName } from '../../createTableName.js'
|
||||
import { buildIndexName } from '../../utilities/buildIndexName.js'
|
||||
import { hasLocalesTable } from '../../utilities/hasLocalesTable.js'
|
||||
import { validateExistingBlockIsIdentical } from '../../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { buildTable } from './build.js'
|
||||
import { createIndex } from './createIndex.js'
|
||||
import { geometryColumn } from './geometryColumn.js'
|
||||
import { idToUUID } from './idToUUID.js'
|
||||
import { parentIDColumnMap } from './parentIDColumnMap.js'
|
||||
import { withDefault } from './withDefault.js'
|
||||
|
||||
type Args = {
|
||||
adapter: BasePostgresAdapter
|
||||
columnPrefix?: string
|
||||
columns: Record<string, PgColumnBuilder>
|
||||
disableNotNull: boolean
|
||||
disableRelsTableUnique?: boolean
|
||||
disableUnique?: boolean
|
||||
fieldPrefix?: string
|
||||
fields: (Field | TabAsField)[]
|
||||
forceLocalized?: boolean
|
||||
indexes: Record<string, (cols: GenericColumns) => IndexBuilder>
|
||||
localesColumns: Record<string, PgColumnBuilder>
|
||||
localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder>
|
||||
newTableName: string
|
||||
parentTableName: string
|
||||
relationships: Set<string>
|
||||
relationsToBuild: RelationMap
|
||||
rootRelationsToBuild?: RelationMap
|
||||
rootTableIDColType: string
|
||||
rootTableName: string
|
||||
uniqueRelationships: Set<string>
|
||||
versions: boolean
|
||||
/**
|
||||
* Tracks whether or not this table is built
|
||||
* from the result of a localized array or block field at some point
|
||||
*/
|
||||
withinLocalizedArrayOrBlock?: boolean
|
||||
}
|
||||
|
||||
type Result = {
|
||||
hasLocalizedField: boolean
|
||||
hasLocalizedManyNumberField: boolean
|
||||
hasLocalizedManyTextField: boolean
|
||||
hasLocalizedRelationshipField: boolean
|
||||
hasManyNumberField: 'index' | boolean
|
||||
hasManyTextField: 'index' | boolean
|
||||
}
|
||||
|
||||
export const traverseFields = ({
|
||||
adapter,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull,
|
||||
disableRelsTableUnique,
|
||||
disableUnique = false,
|
||||
fieldPrefix,
|
||||
fields,
|
||||
forceLocalized,
|
||||
indexes,
|
||||
localesColumns,
|
||||
localesIndexes,
|
||||
newTableName,
|
||||
parentTableName,
|
||||
relationships,
|
||||
relationsToBuild,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
uniqueRelationships,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
}: Args): Result => {
|
||||
const throwValidationError = true
|
||||
let hasLocalizedField = false
|
||||
let hasLocalizedRelationshipField = false
|
||||
let hasManyTextField: 'index' | boolean = false
|
||||
let hasLocalizedManyTextField = false
|
||||
let hasManyNumberField: 'index' | boolean = false
|
||||
let hasLocalizedManyNumberField = false
|
||||
|
||||
let parentIDColType: IDType = 'integer'
|
||||
if (columns.id instanceof PgUUIDBuilder) {
|
||||
parentIDColType = 'uuid'
|
||||
}
|
||||
if (columns.id instanceof PgNumericBuilder) {
|
||||
parentIDColType = 'numeric'
|
||||
}
|
||||
if (columns.id instanceof PgVarcharBuilder) {
|
||||
parentIDColType = 'varchar'
|
||||
}
|
||||
|
||||
fields.forEach((field) => {
|
||||
if ('name' in field && field.name === 'id') {
|
||||
return
|
||||
}
|
||||
if (fieldIsVirtual(field)) {
|
||||
return
|
||||
}
|
||||
|
||||
let columnName: string
|
||||
let fieldName: string
|
||||
|
||||
let targetTable = columns
|
||||
let targetIndexes = indexes
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
columnName = `${columnPrefix || ''}${field.name[0] === '_' ? '_' : ''}${toSnakeCase(
|
||||
field.name,
|
||||
)}`
|
||||
fieldName = `${fieldPrefix?.replace('.', '_') || ''}${field.name}`
|
||||
|
||||
// If field is localized,
|
||||
// add the column to the locale table instead of main table
|
||||
if (
|
||||
adapter.payload.config.localization &&
|
||||
(field.localized || forceLocalized) &&
|
||||
field.type !== 'array' &&
|
||||
field.type !== 'blocks' &&
|
||||
(('hasMany' in field && field.hasMany !== true) || !('hasMany' in field))
|
||||
) {
|
||||
hasLocalizedField = true
|
||||
targetTable = localesColumns
|
||||
targetIndexes = localesIndexes
|
||||
}
|
||||
|
||||
if (
|
||||
(field.unique || field.index || ['relationship', 'upload'].includes(field.type)) &&
|
||||
!['array', 'blocks', 'group'].includes(field.type) &&
|
||||
!('hasMany' in field && field.hasMany === true) &&
|
||||
!('relationTo' in field && Array.isArray(field.relationTo))
|
||||
) {
|
||||
const unique = disableUnique !== true && field.unique
|
||||
if (unique) {
|
||||
const constraintValue = `${fieldPrefix || ''}${field.name}`
|
||||
if (!adapter.fieldConstraints?.[rootTableName]) {
|
||||
adapter.fieldConstraints[rootTableName] = {}
|
||||
}
|
||||
adapter.fieldConstraints[rootTableName][`${columnName}_idx`] = constraintValue
|
||||
}
|
||||
|
||||
const indexName = buildIndexName({ name: `${newTableName}_${columnName}`, adapter })
|
||||
|
||||
targetIndexes[indexName] = createIndex({
|
||||
name: field.localized ? [fieldName, '_locale'] : fieldName,
|
||||
indexName,
|
||||
unique,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
const arrayTableName = createTableName({
|
||||
adapter,
|
||||
config: field,
|
||||
parentTableName: newTableName,
|
||||
prefix: `${newTableName}_`,
|
||||
throwValidationError,
|
||||
versionsCustomName: versions,
|
||||
})
|
||||
|
||||
const baseColumns: Record<string, PgColumnBuilder> = {
|
||||
_order: integer('_order').notNull(),
|
||||
_parentID: parentIDColumnMap[parentIDColType]('_parent_id').notNull(),
|
||||
}
|
||||
|
||||
const baseExtraConfig: BaseExtraConfig = {
|
||||
_orderIdx: (cols) => index(`${arrayTableName}_order_idx`).on(cols._order),
|
||||
_parentIDFk: (cols) =>
|
||||
foreignKey({
|
||||
name: `${arrayTableName}_parent_id_fk`,
|
||||
columns: [cols['_parentID']],
|
||||
foreignColumns: [adapter.tables[parentTableName].id],
|
||||
}).onDelete('cascade'),
|
||||
_parentIDIdx: (cols) => index(`${arrayTableName}_parent_id_idx`).on(cols._parentID),
|
||||
}
|
||||
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
||||
baseExtraConfig._localeIdx = (cols) =>
|
||||
index(`${arrayTableName}_locale_idx`).on(cols._locale)
|
||||
}
|
||||
|
||||
const {
|
||||
hasLocalizedManyNumberField: subHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: subHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: subHasLocalizedRelationshipField,
|
||||
hasManyNumberField: subHasManyNumberField,
|
||||
hasManyTextField: subHasManyTextField,
|
||||
relationsToBuild: subRelationsToBuild,
|
||||
} = buildTable({
|
||||
adapter,
|
||||
baseColumns,
|
||||
baseExtraConfig,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableRelsTableUnique: true,
|
||||
disableUnique,
|
||||
fields: disableUnique ? idToUUID(field.fields) : field.fields,
|
||||
rootRelationships: relationships,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
rootUniqueRelationships: uniqueRelationships,
|
||||
tableName: arrayTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock: isLocalized,
|
||||
})
|
||||
|
||||
if (subHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = subHasLocalizedManyNumberField
|
||||
}
|
||||
|
||||
if (subHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = subHasLocalizedRelationshipField
|
||||
}
|
||||
|
||||
if (subHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = subHasLocalizedManyTextField
|
||||
}
|
||||
|
||||
if (subHasManyTextField) {
|
||||
if (!hasManyTextField || subHasManyTextField === 'index') {
|
||||
hasManyTextField = subHasManyTextField
|
||||
}
|
||||
}
|
||||
if (subHasManyNumberField) {
|
||||
if (!hasManyNumberField || subHasManyNumberField === 'index') {
|
||||
hasManyNumberField = subHasManyNumberField
|
||||
}
|
||||
}
|
||||
|
||||
relationsToBuild.set(fieldName, {
|
||||
type: 'many',
|
||||
// arrays have their own localized table, independent of the base table.
|
||||
localized: false,
|
||||
target: arrayTableName,
|
||||
})
|
||||
|
||||
adapter.relations[`relations_${arrayTableName}`] = relations(
|
||||
adapter.tables[arrayTableName],
|
||||
({ many, one }) => {
|
||||
const result: Record<string, Relation<string>> = {
|
||||
_parentID: one(adapter.tables[parentTableName], {
|
||||
fields: [adapter.tables[arrayTableName]._parentID],
|
||||
references: [adapter.tables[parentTableName].id],
|
||||
relationName: fieldName,
|
||||
}),
|
||||
}
|
||||
|
||||
if (hasLocalesTable(field.fields)) {
|
||||
result._locales = many(adapter.tables[`${arrayTableName}${adapter.localesSuffix}`], {
|
||||
relationName: '_locales',
|
||||
})
|
||||
}
|
||||
|
||||
subRelationsToBuild.forEach(({ type, localized, target }, key) => {
|
||||
if (type === 'one') {
|
||||
const arrayWithLocalized = localized
|
||||
? `${arrayTableName}${adapter.localesSuffix}`
|
||||
: arrayTableName
|
||||
result[key] = one(adapter.tables[target], {
|
||||
fields: [adapter.tables[arrayWithLocalized][key]],
|
||||
references: [adapter.tables[target].id],
|
||||
relationName: key,
|
||||
})
|
||||
}
|
||||
if (type === 'many') {
|
||||
result[key] = many(adapter.tables[target], { relationName: key })
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
},
|
||||
)
|
||||
|
||||
break
|
||||
}
|
||||
case 'blocks': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
field.blocks.forEach((block) => {
|
||||
const blockTableName = createTableName({
|
||||
adapter,
|
||||
config: block,
|
||||
parentTableName: rootTableName,
|
||||
prefix: `${rootTableName}_blocks_`,
|
||||
throwValidationError,
|
||||
versionsCustomName: versions,
|
||||
})
|
||||
if (!adapter.tables[blockTableName]) {
|
||||
const baseColumns: Record<string, PgColumnBuilder> = {
|
||||
_order: integer('_order').notNull(),
|
||||
_parentID: parentIDColumnMap[rootTableIDColType]('_parent_id').notNull(),
|
||||
_path: text('_path').notNull(),
|
||||
}
|
||||
|
||||
const baseExtraConfig: BaseExtraConfig = {
|
||||
_orderIdx: (cols) => index(`${blockTableName}_order_idx`).on(cols._order),
|
||||
_parentIdFk: (cols) =>
|
||||
foreignKey({
|
||||
name: `${blockTableName}_parent_id_fk`,
|
||||
columns: [cols._parentID],
|
||||
foreignColumns: [adapter.tables[rootTableName].id],
|
||||
}).onDelete('cascade'),
|
||||
_parentIDIdx: (cols) => index(`${blockTableName}_parent_id_idx`).on(cols._parentID),
|
||||
_pathIdx: (cols) => index(`${blockTableName}_path_idx`).on(cols._path),
|
||||
}
|
||||
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
|
||||
baseExtraConfig._localeIdx = (cols) =>
|
||||
index(`${blockTableName}_locale_idx`).on(cols._locale)
|
||||
}
|
||||
|
||||
const {
|
||||
hasLocalizedManyNumberField: subHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: subHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: subHasLocalizedRelationshipField,
|
||||
hasManyNumberField: subHasManyNumberField,
|
||||
hasManyTextField: subHasManyTextField,
|
||||
relationsToBuild: subRelationsToBuild,
|
||||
} = buildTable({
|
||||
adapter,
|
||||
baseColumns,
|
||||
baseExtraConfig,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableRelsTableUnique: true,
|
||||
disableUnique,
|
||||
fields: disableUnique ? idToUUID(block.fields) : block.fields,
|
||||
rootRelationships: relationships,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
rootUniqueRelationships: uniqueRelationships,
|
||||
tableName: blockTableName,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock: isLocalized,
|
||||
})
|
||||
|
||||
if (subHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = subHasLocalizedManyNumberField
|
||||
}
|
||||
|
||||
if (subHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = subHasLocalizedRelationshipField
|
||||
}
|
||||
|
||||
if (subHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = subHasLocalizedManyTextField
|
||||
}
|
||||
|
||||
if (subHasManyTextField) {
|
||||
if (!hasManyTextField || subHasManyTextField === 'index') {
|
||||
hasManyTextField = subHasManyTextField
|
||||
}
|
||||
}
|
||||
|
||||
if (subHasManyNumberField) {
|
||||
if (!hasManyNumberField || subHasManyNumberField === 'index') {
|
||||
hasManyNumberField = subHasManyNumberField
|
||||
}
|
||||
}
|
||||
|
||||
adapter.relations[`relations_${blockTableName}`] = relations(
|
||||
adapter.tables[blockTableName],
|
||||
({ many, one }) => {
|
||||
const result: Record<string, Relation<string>> = {
|
||||
_parentID: one(adapter.tables[rootTableName], {
|
||||
fields: [adapter.tables[blockTableName]._parentID],
|
||||
references: [adapter.tables[rootTableName].id],
|
||||
relationName: `_blocks_${block.slug}`,
|
||||
}),
|
||||
}
|
||||
|
||||
if (hasLocalesTable(block.fields)) {
|
||||
result._locales = many(
|
||||
adapter.tables[`${blockTableName}${adapter.localesSuffix}`],
|
||||
{ relationName: '_locales' },
|
||||
)
|
||||
}
|
||||
|
||||
subRelationsToBuild.forEach(({ type, localized, target }, key) => {
|
||||
if (type === 'one') {
|
||||
const blockWithLocalized = localized
|
||||
? `${blockTableName}${adapter.localesSuffix}`
|
||||
: blockTableName
|
||||
result[key] = one(adapter.tables[target], {
|
||||
fields: [adapter.tables[blockWithLocalized][key]],
|
||||
references: [adapter.tables[target].id],
|
||||
relationName: key,
|
||||
})
|
||||
}
|
||||
if (type === 'many') {
|
||||
result[key] = many(adapter.tables[target], { relationName: key })
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
},
|
||||
)
|
||||
} else if (process.env.NODE_ENV !== 'production' && !versions) {
|
||||
validateExistingBlockIsIdentical({
|
||||
block,
|
||||
localized: field.localized,
|
||||
rootTableName,
|
||||
table: adapter.tables[blockTableName],
|
||||
tableLocales: adapter.tables[`${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}`, {
|
||||
type: 'many',
|
||||
// blocks are not localized on the parent table
|
||||
localized: false,
|
||||
target: blockTableName,
|
||||
})
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
case 'checkbox': {
|
||||
targetTable[fieldName] = withDefault(boolean(columnName), field)
|
||||
break
|
||||
}
|
||||
case 'code':
|
||||
|
||||
case 'email':
|
||||
|
||||
case 'textarea': {
|
||||
targetTable[fieldName] = withDefault(varchar(columnName), field)
|
||||
break
|
||||
}
|
||||
case 'collapsible':
|
||||
|
||||
case 'row': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
const {
|
||||
hasLocalizedField: rowHasLocalizedField,
|
||||
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: rowHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: rowHasLocalizedRelationshipField,
|
||||
hasManyNumberField: rowHasManyNumberField,
|
||||
hasManyTextField: rowHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fieldPrefix,
|
||||
fields: field.fields,
|
||||
forceLocalized,
|
||||
indexes,
|
||||
localesColumns,
|
||||
localesIndexes,
|
||||
newTableName,
|
||||
parentTableName,
|
||||
relationships,
|
||||
relationsToBuild,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
uniqueRelationships,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
|
||||
if (rowHasLocalizedField) {
|
||||
hasLocalizedField = true
|
||||
}
|
||||
if (rowHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = true
|
||||
}
|
||||
if (rowHasManyTextField) {
|
||||
hasManyTextField = true
|
||||
}
|
||||
if (rowHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
if (rowHasManyNumberField) {
|
||||
hasManyNumberField = true
|
||||
}
|
||||
if (rowHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = true
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'date': {
|
||||
targetTable[fieldName] = withDefault(
|
||||
timestamp(columnName, {
|
||||
mode: 'string',
|
||||
precision: 3,
|
||||
withTimezone: true,
|
||||
}),
|
||||
field,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case 'group':
|
||||
case 'tab': {
|
||||
if (!('name' in field)) {
|
||||
const {
|
||||
hasLocalizedField: groupHasLocalizedField,
|
||||
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: groupHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: groupHasLocalizedRelationshipField,
|
||||
hasManyNumberField: groupHasManyNumberField,
|
||||
hasManyTextField: groupHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull,
|
||||
disableUnique,
|
||||
fieldPrefix,
|
||||
fields: field.fields,
|
||||
forceLocalized,
|
||||
indexes,
|
||||
localesColumns,
|
||||
localesIndexes,
|
||||
newTableName,
|
||||
parentTableName,
|
||||
relationships,
|
||||
relationsToBuild,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
uniqueRelationships,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
|
||||
if (groupHasLocalizedField) {
|
||||
hasLocalizedField = true
|
||||
}
|
||||
if (groupHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = true
|
||||
}
|
||||
if (groupHasManyTextField) {
|
||||
hasManyTextField = true
|
||||
}
|
||||
if (groupHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
if (groupHasManyNumberField) {
|
||||
hasManyNumberField = true
|
||||
}
|
||||
if (groupHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = true
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
const {
|
||||
hasLocalizedField: groupHasLocalizedField,
|
||||
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: groupHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: groupHasLocalizedRelationshipField,
|
||||
hasManyNumberField: groupHasManyNumberField,
|
||||
hasManyTextField: groupHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
columnPrefix: `${columnName}_`,
|
||||
columns,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fieldPrefix: `${fieldName}.`,
|
||||
fields: field.fields,
|
||||
forceLocalized: field.localized,
|
||||
indexes,
|
||||
localesColumns,
|
||||
localesIndexes,
|
||||
newTableName: `${parentTableName}_${columnName}`,
|
||||
parentTableName,
|
||||
relationships,
|
||||
relationsToBuild,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
uniqueRelationships,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized,
|
||||
})
|
||||
|
||||
if (groupHasLocalizedField) {
|
||||
hasLocalizedField = true
|
||||
}
|
||||
if (groupHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = true
|
||||
}
|
||||
if (groupHasManyTextField) {
|
||||
hasManyTextField = true
|
||||
}
|
||||
if (groupHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
if (groupHasManyNumberField) {
|
||||
hasManyNumberField = true
|
||||
}
|
||||
if (groupHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = true
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'json':
|
||||
|
||||
case 'richText': {
|
||||
targetTable[fieldName] = withDefault(jsonb(columnName), field)
|
||||
break
|
||||
}
|
||||
|
||||
case 'number': {
|
||||
if (field.hasMany) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
hasLocalizedManyNumberField = true
|
||||
}
|
||||
|
||||
if (field.index) {
|
||||
hasManyNumberField = 'index'
|
||||
} else if (!hasManyNumberField) {
|
||||
hasManyNumberField = true
|
||||
}
|
||||
|
||||
if (field.unique) {
|
||||
throw new InvalidConfiguration(
|
||||
'Unique is not supported in Postgres for hasMany number fields.',
|
||||
)
|
||||
}
|
||||
} else {
|
||||
targetTable[fieldName] = withDefault(numeric(columnName), field)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'point': {
|
||||
targetTable[fieldName] = withDefault(geometryColumn(columnName), field)
|
||||
if (!adapter.extensions.postgis) {
|
||||
adapter.extensions.postgis = true
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'radio':
|
||||
|
||||
case 'select': {
|
||||
const enumName = createTableName({
|
||||
adapter,
|
||||
config: field,
|
||||
parentTableName: newTableName,
|
||||
prefix: `enum_${newTableName}_`,
|
||||
target: 'enumName',
|
||||
throwValidationError,
|
||||
})
|
||||
|
||||
adapter.enums[enumName] = adapter.pgSchema.enum(
|
||||
enumName,
|
||||
field.options.map((option) => {
|
||||
if (optionIsObject(option)) {
|
||||
return option.value
|
||||
}
|
||||
|
||||
return option
|
||||
}) as [string, ...string[]],
|
||||
)
|
||||
|
||||
if (field.type === 'select' && field.hasMany) {
|
||||
const selectTableName = createTableName({
|
||||
adapter,
|
||||
config: field,
|
||||
parentTableName: newTableName,
|
||||
prefix: `${newTableName}_`,
|
||||
throwValidationError,
|
||||
versionsCustomName: versions,
|
||||
})
|
||||
const baseColumns: Record<string, PgColumnBuilder> = {
|
||||
order: integer('order').notNull(),
|
||||
parent: parentIDColumnMap[parentIDColType]('parent_id').notNull(),
|
||||
value: adapter.enums[enumName]('value'),
|
||||
}
|
||||
|
||||
const baseExtraConfig: BaseExtraConfig = {
|
||||
orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order),
|
||||
parentFk: (cols) =>
|
||||
foreignKey({
|
||||
name: `${selectTableName}_parent_fk`,
|
||||
columns: [cols.parent],
|
||||
foreignColumns: [adapter.tables[parentTableName].id],
|
||||
}).onDelete('cascade'),
|
||||
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
|
||||
}
|
||||
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
baseColumns.locale = adapter.enums.enum__locales('locale').notNull()
|
||||
baseExtraConfig.localeIdx = (cols) =>
|
||||
index(`${selectTableName}_locale_idx`).on(cols.locale)
|
||||
}
|
||||
|
||||
if (field.index) {
|
||||
baseExtraConfig.value = (cols) => index(`${selectTableName}_value_idx`).on(cols.value)
|
||||
}
|
||||
|
||||
buildTable({
|
||||
adapter,
|
||||
baseColumns,
|
||||
baseExtraConfig,
|
||||
disableNotNull,
|
||||
disableUnique,
|
||||
fields: [],
|
||||
rootTableName,
|
||||
tableName: selectTableName,
|
||||
versions,
|
||||
})
|
||||
|
||||
relationsToBuild.set(fieldName, {
|
||||
type: 'many',
|
||||
// selects have their own localized table, independent of the base table.
|
||||
localized: false,
|
||||
target: selectTableName,
|
||||
})
|
||||
|
||||
adapter.relations[`relations_${selectTableName}`] = relations(
|
||||
adapter.tables[selectTableName],
|
||||
({ one }) => ({
|
||||
parent: one(adapter.tables[parentTableName], {
|
||||
fields: [adapter.tables[selectTableName].parent],
|
||||
references: [adapter.tables[parentTableName].id],
|
||||
relationName: fieldName,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
targetTable[fieldName] = withDefault(adapter.enums[enumName](columnName), field)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
case 'upload':
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
field.relationTo.forEach((relation) => {
|
||||
relationships.add(relation)
|
||||
if (field.unique && !disableUnique && !disableRelsTableUnique) {
|
||||
uniqueRelationships.add(relation)
|
||||
}
|
||||
})
|
||||
} else if (field.hasMany) {
|
||||
relationships.add(field.relationTo)
|
||||
if (field.unique && !disableUnique && !disableRelsTableUnique) {
|
||||
uniqueRelationships.add(field.relationTo)
|
||||
}
|
||||
} else {
|
||||
// simple relationships get a column on the targetTable with a foreign key to the relationTo table
|
||||
const relationshipConfig = adapter.payload.collections[field.relationTo].config
|
||||
|
||||
const tableName = adapter.tableNameMap.get(toSnakeCase(field.relationTo))
|
||||
|
||||
// get the id type of the related collection
|
||||
let colType = adapter.idType === 'uuid' ? 'uuid' : 'integer'
|
||||
const relatedCollectionCustomID = relationshipConfig.fields.find(
|
||||
(field) => fieldAffectsData(field) && field.name === 'id',
|
||||
)
|
||||
if (relatedCollectionCustomID?.type === 'number') {
|
||||
colType = 'numeric'
|
||||
}
|
||||
if (relatedCollectionCustomID?.type === 'text') {
|
||||
colType = 'varchar'
|
||||
}
|
||||
|
||||
// make the foreign key column for relationship using the correct id column type
|
||||
targetTable[fieldName] = parentIDColumnMap[colType](`${columnName}_id`).references(
|
||||
() => adapter.tables[tableName].id,
|
||||
{ onDelete: 'set null' },
|
||||
)
|
||||
|
||||
// add relationship to table
|
||||
relationsToBuild.set(fieldName, {
|
||||
type: 'one',
|
||||
localized: adapter.payload.config.localization && (field.localized || forceLocalized),
|
||||
target: tableName,
|
||||
})
|
||||
|
||||
// add notNull when not required
|
||||
if (!disableNotNull && field.required && !field.admin?.condition) {
|
||||
targetTable[fieldName].notNull()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if (
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock
|
||||
) {
|
||||
hasLocalizedRelationshipField = true
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case 'tabs': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
const {
|
||||
hasLocalizedField: tabHasLocalizedField,
|
||||
hasLocalizedManyNumberField: tabHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: tabHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: tabHasLocalizedRelationshipField,
|
||||
hasManyNumberField: tabHasManyNumberField,
|
||||
hasManyTextField: tabHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fieldPrefix,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
forceLocalized,
|
||||
indexes,
|
||||
localesColumns,
|
||||
localesIndexes,
|
||||
newTableName,
|
||||
parentTableName,
|
||||
relationships,
|
||||
relationsToBuild,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
uniqueRelationships,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
|
||||
if (tabHasLocalizedField) {
|
||||
hasLocalizedField = true
|
||||
}
|
||||
if (tabHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = true
|
||||
}
|
||||
if (tabHasManyTextField) {
|
||||
hasManyTextField = true
|
||||
}
|
||||
if (tabHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
if (tabHasManyNumberField) {
|
||||
hasManyNumberField = true
|
||||
}
|
||||
if (tabHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = true
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'text': {
|
||||
if (field.hasMany) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
|
||||
if (field.index) {
|
||||
hasManyTextField = 'index'
|
||||
} else if (!hasManyTextField) {
|
||||
hasManyTextField = true
|
||||
}
|
||||
|
||||
if (field.unique) {
|
||||
throw new InvalidConfiguration(
|
||||
'Unique is not supported in Postgres for hasMany text fields.',
|
||||
)
|
||||
}
|
||||
} else {
|
||||
targetTable[fieldName] = withDefault(varchar(columnName), field)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
const condition = field.admin && field.admin.condition
|
||||
|
||||
if (
|
||||
!disableNotNull &&
|
||||
targetTable[fieldName] &&
|
||||
'required' in field &&
|
||||
field.required &&
|
||||
!condition
|
||||
) {
|
||||
targetTable[fieldName].notNull()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
hasLocalizedField,
|
||||
hasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField,
|
||||
hasManyNumberField,
|
||||
hasManyTextField,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { SQL } from 'drizzle-orm'
|
||||
import type { Field, Where } from 'payload'
|
||||
|
||||
import type { DrizzleAdapter, GenericColumn } from '../types.js'
|
||||
import type { BuildQueryJoinAliases } from './buildQuery.js'
|
||||
|
||||
import { parseParams } from './parseParams.js'
|
||||
|
||||
export function buildAndOrConditions({
|
||||
adapter,
|
||||
fields,
|
||||
joins,
|
||||
locale,
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
}: {
|
||||
adapter: DrizzleAdapter
|
||||
collectionSlug?: string
|
||||
fields: Field[]
|
||||
globalSlug?: string
|
||||
joins: BuildQueryJoinAliases
|
||||
locale?: string
|
||||
selectFields: Record<string, GenericColumn>
|
||||
tableName: string
|
||||
where: Where[]
|
||||
}): SQL[] {
|
||||
const completedConditions = []
|
||||
// Loop over all AND / OR operations and add them to the AND / OR query param
|
||||
// Operations should come through as an array
|
||||
|
||||
for (const condition of where) {
|
||||
// If the operation is properly formatted as an object
|
||||
if (typeof condition === 'object') {
|
||||
const result = parseParams({
|
||||
adapter,
|
||||
fields,
|
||||
joins,
|
||||
locale,
|
||||
selectFields,
|
||||
tableName,
|
||||
where: condition,
|
||||
})
|
||||
if (result && Object.keys(result).length > 0) {
|
||||
completedConditions.push(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
return completedConditions
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import { validOperators } from 'payload/shared'
|
||||
import type { DrizzleAdapter, GenericColumn } from '../types.js'
|
||||
import type { BuildQueryJoinAliases } from './buildQuery.js'
|
||||
|
||||
import { buildAndOrConditions } from './buildAndOrConditions.js'
|
||||
import { getTableColumnFromPath } from './getTableColumnFromPath.js'
|
||||
import { sanitizeQueryValue } from './sanitizeQueryValue.js'
|
||||
|
||||
@@ -349,3 +348,46 @@ export function parseParams({
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function buildAndOrConditions({
|
||||
adapter,
|
||||
fields,
|
||||
joins,
|
||||
locale,
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
}: {
|
||||
adapter: DrizzleAdapter
|
||||
collectionSlug?: string
|
||||
fields: Field[]
|
||||
globalSlug?: string
|
||||
joins: BuildQueryJoinAliases
|
||||
locale?: string
|
||||
selectFields: Record<string, GenericColumn>
|
||||
tableName: string
|
||||
where: Where[]
|
||||
}): SQL[] {
|
||||
const completedConditions = []
|
||||
// Loop over all AND / OR operations and add them to the AND / OR query param
|
||||
// Operations should come through as an array
|
||||
|
||||
for (const condition of where) {
|
||||
// If the operation is properly formatted as an object
|
||||
if (typeof condition === 'object') {
|
||||
const result = parseParams({
|
||||
adapter,
|
||||
fields,
|
||||
joins,
|
||||
locale,
|
||||
selectFields,
|
||||
tableName,
|
||||
where: condition,
|
||||
})
|
||||
if (result && Object.keys(result).length > 0) {
|
||||
completedConditions.push(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
return completedConditions
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import type { ArrayField } from 'payload'
|
||||
|
||||
import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types.js'
|
||||
|
||||
import { isArrayOfRows } from '../../utilities/isArrayOfRows.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args = {
|
||||
adapter: DrizzleAdapter
|
||||
arrayTableName: string
|
||||
baseTableName: string
|
||||
blocks: {
|
||||
[blockType: string]: BlockRowToInsert[]
|
||||
}
|
||||
blocksToDelete: Set<string>
|
||||
data: unknown
|
||||
field: ArrayField
|
||||
locale?: string
|
||||
numbers: Record<string, unknown>[]
|
||||
path: string
|
||||
relationships: Record<string, unknown>[]
|
||||
relationshipsToDelete: RelationshipToDelete[]
|
||||
selects: {
|
||||
[tableName: string]: Record<string, unknown>[]
|
||||
}
|
||||
texts: Record<string, unknown>[]
|
||||
/**
|
||||
* Set to a locale code if this set of fields is traversed within a
|
||||
* localized array or block field
|
||||
*/
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
|
||||
export const transformArray = ({
|
||||
adapter,
|
||||
arrayTableName,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
data,
|
||||
field,
|
||||
locale,
|
||||
numbers,
|
||||
path,
|
||||
relationships,
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
const newRows: ArrayRowToInsert[] = []
|
||||
|
||||
const hasUUID = adapter.tables[arrayTableName]._uuid
|
||||
|
||||
if (isArrayOfRows(data)) {
|
||||
data.forEach((arrayRow, i) => {
|
||||
const newRow: ArrayRowToInsert = {
|
||||
arrays: {},
|
||||
locales: {},
|
||||
row: {
|
||||
_order: i + 1,
|
||||
},
|
||||
}
|
||||
|
||||
// If we have declared a _uuid field on arrays,
|
||||
// that means the ID has to be unique,
|
||||
// and our ids within arrays are not unique.
|
||||
// So move the ID to a uuid field for storage
|
||||
// and allow the database to generate a serial id automatically
|
||||
if (hasUUID) {
|
||||
newRow.row._uuid = arrayRow.id
|
||||
delete arrayRow.id
|
||||
}
|
||||
|
||||
if (locale) {
|
||||
newRow.locales[locale] = {
|
||||
_locale: locale,
|
||||
}
|
||||
}
|
||||
|
||||
if (field.localized) {
|
||||
newRow.row._locale = locale
|
||||
}
|
||||
|
||||
if (withinArrayOrBlockLocale) {
|
||||
newRow.row._locale = withinArrayOrBlockLocale
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays: newRow.arrays,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
columnPrefix: '',
|
||||
data: arrayRow,
|
||||
fieldPrefix: '',
|
||||
fields: field.fields,
|
||||
locales: newRow.locales,
|
||||
numbers,
|
||||
parentTableName: arrayTableName,
|
||||
path: `${path || ''}${field.name}.${i}.`,
|
||||
relationships,
|
||||
relationshipsToDelete,
|
||||
row: newRow.row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
newRows.push(newRow)
|
||||
})
|
||||
}
|
||||
|
||||
return newRows
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import type { BlocksField } from 'payload'
|
||||
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { BlockRowToInsert, RelationshipToDelete } from './types.js'
|
||||
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args = {
|
||||
adapter: DrizzleAdapter
|
||||
baseTableName: string
|
||||
blocks: {
|
||||
[blockType: string]: BlockRowToInsert[]
|
||||
}
|
||||
blocksToDelete: Set<string>
|
||||
data: Record<string, unknown>[]
|
||||
field: BlocksField
|
||||
locale?: string
|
||||
numbers: Record<string, unknown>[]
|
||||
path: string
|
||||
relationships: Record<string, unknown>[]
|
||||
relationshipsToDelete: RelationshipToDelete[]
|
||||
selects: {
|
||||
[tableName: string]: Record<string, unknown>[]
|
||||
}
|
||||
texts: Record<string, unknown>[]
|
||||
/**
|
||||
* Set to a locale code if this set of fields is traversed within a
|
||||
* localized array or block field
|
||||
*/
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
export const transformBlocks = ({
|
||||
adapter,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
data,
|
||||
field,
|
||||
locale,
|
||||
numbers,
|
||||
path,
|
||||
relationships,
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
data.forEach((blockRow, i) => {
|
||||
if (typeof blockRow.blockType !== 'string') {
|
||||
return
|
||||
}
|
||||
const matchedBlock = field.blocks.find(({ slug }) => slug === blockRow.blockType)
|
||||
if (!matchedBlock) {
|
||||
return
|
||||
}
|
||||
const blockType = toSnakeCase(blockRow.blockType)
|
||||
|
||||
if (!blocks[blockType]) {
|
||||
blocks[blockType] = []
|
||||
}
|
||||
|
||||
const newRow: BlockRowToInsert = {
|
||||
arrays: {},
|
||||
locales: {},
|
||||
row: {
|
||||
_order: i + 1,
|
||||
_path: `${path}${field.name}`,
|
||||
},
|
||||
}
|
||||
|
||||
if (field.localized && locale) {
|
||||
newRow.row._locale = locale
|
||||
}
|
||||
if (withinArrayOrBlockLocale) {
|
||||
newRow.row._locale = withinArrayOrBlockLocale
|
||||
}
|
||||
|
||||
const blockTableName = adapter.tableNameMap.get(`${baseTableName}_blocks_${blockType}`)
|
||||
|
||||
const hasUUID = adapter.tables[blockTableName]._uuid
|
||||
|
||||
// If we have declared a _uuid field on arrays,
|
||||
// that means the ID has to be unique,
|
||||
// and our ids within arrays are not unique.
|
||||
// So move the ID to a uuid field for storage
|
||||
// and allow the database to generate a serial id automatically
|
||||
if (hasUUID) {
|
||||
newRow.row._uuid = blockRow.id
|
||||
delete blockRow.id
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays: newRow.arrays,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
columnPrefix: '',
|
||||
data: blockRow,
|
||||
fieldPrefix: '',
|
||||
fields: matchedBlock.fields,
|
||||
locales: newRow.locales,
|
||||
numbers,
|
||||
parentTableName: blockTableName,
|
||||
path: `${path || ''}${field.name}.${i}.`,
|
||||
relationships,
|
||||
relationshipsToDelete,
|
||||
row: newRow.row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
blocks[blockType].push(newRow)
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Field } from 'payload'
|
||||
import type { ArrayField, BlocksField, Field } from 'payload'
|
||||
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { fieldAffectsData, fieldIsVirtual } from 'payload/shared'
|
||||
@@ -8,8 +8,6 @@ import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types.js'
|
||||
|
||||
import { isArrayOfRows } from '../../utilities/isArrayOfRows.js'
|
||||
import { transformArray } from './array.js'
|
||||
import { transformBlocks } from './blocks.js'
|
||||
import { transformNumbers } from './numbers.js'
|
||||
import { transformRelationship } from './relationships.js'
|
||||
import { transformSelects } from './selects.js'
|
||||
@@ -617,3 +615,226 @@ export const traverseFields = ({
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type TransformArrayArgs = {
|
||||
adapter: DrizzleAdapter
|
||||
arrayTableName: string
|
||||
baseTableName: string
|
||||
blocks: {
|
||||
[blockType: string]: BlockRowToInsert[]
|
||||
}
|
||||
blocksToDelete: Set<string>
|
||||
data: unknown
|
||||
field: ArrayField
|
||||
locale?: string
|
||||
numbers: Record<string, unknown>[]
|
||||
path: string
|
||||
relationships: Record<string, unknown>[]
|
||||
relationshipsToDelete: RelationshipToDelete[]
|
||||
selects: {
|
||||
[tableName: string]: Record<string, unknown>[]
|
||||
}
|
||||
texts: Record<string, unknown>[]
|
||||
/**
|
||||
* Set to a locale code if this set of fields is traversed within a
|
||||
* localized array or block field
|
||||
*/
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
|
||||
// Can't be in a separate file to avoid circular import between traverseFields and transformArray
|
||||
export const transformArray = ({
|
||||
adapter,
|
||||
arrayTableName,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
data,
|
||||
field,
|
||||
locale,
|
||||
numbers,
|
||||
path,
|
||||
relationships,
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
}: TransformArrayArgs) => {
|
||||
const newRows: ArrayRowToInsert[] = []
|
||||
|
||||
const hasUUID = adapter.tables[arrayTableName]._uuid
|
||||
|
||||
if (isArrayOfRows(data)) {
|
||||
data.forEach((arrayRow, i) => {
|
||||
const newRow: ArrayRowToInsert = {
|
||||
arrays: {},
|
||||
locales: {},
|
||||
row: {
|
||||
_order: i + 1,
|
||||
},
|
||||
}
|
||||
|
||||
// If we have declared a _uuid field on arrays,
|
||||
// that means the ID has to be unique,
|
||||
// and our ids within arrays are not unique.
|
||||
// So move the ID to a uuid field for storage
|
||||
// and allow the database to generate a serial id automatically
|
||||
if (hasUUID) {
|
||||
newRow.row._uuid = arrayRow.id
|
||||
delete arrayRow.id
|
||||
}
|
||||
|
||||
if (locale) {
|
||||
newRow.locales[locale] = {
|
||||
_locale: locale,
|
||||
}
|
||||
}
|
||||
|
||||
if (field.localized) {
|
||||
newRow.row._locale = locale
|
||||
}
|
||||
|
||||
if (withinArrayOrBlockLocale) {
|
||||
newRow.row._locale = withinArrayOrBlockLocale
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays: newRow.arrays,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
columnPrefix: '',
|
||||
data: arrayRow,
|
||||
fieldPrefix: '',
|
||||
fields: field.fields,
|
||||
locales: newRow.locales,
|
||||
numbers,
|
||||
parentTableName: arrayTableName,
|
||||
path: `${path || ''}${field.name}.${i}.`,
|
||||
relationships,
|
||||
relationshipsToDelete,
|
||||
row: newRow.row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
newRows.push(newRow)
|
||||
})
|
||||
}
|
||||
|
||||
return newRows
|
||||
}
|
||||
|
||||
type TransformBlocksArgs = {
|
||||
adapter: DrizzleAdapter
|
||||
baseTableName: string
|
||||
blocks: {
|
||||
[blockType: string]: BlockRowToInsert[]
|
||||
}
|
||||
blocksToDelete: Set<string>
|
||||
data: Record<string, unknown>[]
|
||||
field: BlocksField
|
||||
locale?: string
|
||||
numbers: Record<string, unknown>[]
|
||||
path: string
|
||||
relationships: Record<string, unknown>[]
|
||||
relationshipsToDelete: RelationshipToDelete[]
|
||||
selects: {
|
||||
[tableName: string]: Record<string, unknown>[]
|
||||
}
|
||||
texts: Record<string, unknown>[]
|
||||
/**
|
||||
* Set to a locale code if this set of fields is traversed within a
|
||||
* localized array or block field
|
||||
*/
|
||||
withinArrayOrBlockLocale?: string
|
||||
}
|
||||
|
||||
// Can't be in a separate file to avoid circular import between traverseFields and transformBlocks
|
||||
export const transformBlocks = ({
|
||||
adapter,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
data,
|
||||
field,
|
||||
locale,
|
||||
numbers,
|
||||
path,
|
||||
relationships,
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
}: TransformBlocksArgs) => {
|
||||
data.forEach((blockRow, i) => {
|
||||
if (typeof blockRow.blockType !== 'string') {
|
||||
return
|
||||
}
|
||||
const matchedBlock = field.blocks.find(({ slug }) => slug === blockRow.blockType)
|
||||
if (!matchedBlock) {
|
||||
return
|
||||
}
|
||||
const blockType = toSnakeCase(blockRow.blockType)
|
||||
|
||||
if (!blocks[blockType]) {
|
||||
blocks[blockType] = []
|
||||
}
|
||||
|
||||
const newRow: BlockRowToInsert = {
|
||||
arrays: {},
|
||||
locales: {},
|
||||
row: {
|
||||
_order: i + 1,
|
||||
_path: `${path}${field.name}`,
|
||||
},
|
||||
}
|
||||
|
||||
if (field.localized && locale) {
|
||||
newRow.row._locale = locale
|
||||
}
|
||||
if (withinArrayOrBlockLocale) {
|
||||
newRow.row._locale = withinArrayOrBlockLocale
|
||||
}
|
||||
|
||||
const blockTableName = adapter.tableNameMap.get(`${baseTableName}_blocks_${blockType}`)
|
||||
|
||||
const hasUUID = adapter.tables[blockTableName]._uuid
|
||||
|
||||
// If we have declared a _uuid field on arrays,
|
||||
// that means the ID has to be unique,
|
||||
// and our ids within arrays are not unique.
|
||||
// So move the ID to a uuid field for storage
|
||||
// and allow the database to generate a serial id automatically
|
||||
if (hasUUID) {
|
||||
newRow.row._uuid = blockRow.id
|
||||
delete blockRow.id
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays: newRow.arrays,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
columnPrefix: '',
|
||||
data: blockRow,
|
||||
fieldPrefix: '',
|
||||
fields: matchedBlock.fields,
|
||||
locales: newRow.locales,
|
||||
numbers,
|
||||
parentTableName: blockTableName,
|
||||
path: `${path || ''}${field.name}.${i}.`,
|
||||
relationships,
|
||||
relationshipsToDelete,
|
||||
row: newRow.row,
|
||||
selects,
|
||||
texts,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
blocks[blockType].push(newRow)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
CollapsibleField,
|
||||
DateField,
|
||||
EmailField,
|
||||
FieldWithSubFields,
|
||||
GroupField,
|
||||
JSONField,
|
||||
NumberField,
|
||||
@@ -21,11 +22,11 @@ import type {
|
||||
} from 'payload'
|
||||
|
||||
import { GraphQLEnumType, GraphQLInputObjectType } from 'graphql'
|
||||
import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared'
|
||||
|
||||
import { GraphQLJSON } from '../packages/graphql-type-json/index.js'
|
||||
import { combineParentName } from '../utilities/combineParentName.js'
|
||||
import { formatName } from '../utilities/formatName.js'
|
||||
import { recursivelyBuildNestedPaths } from './recursivelyBuildNestedPaths.js'
|
||||
import { withOperators } from './withOperators.js'
|
||||
|
||||
type Args = {
|
||||
@@ -161,3 +162,82 @@ export const fieldToSchemaMap = ({ nestedFieldName, parentName }: Args): any =>
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
type RecursivelyBuildNestedPathsArgs = {
|
||||
field: FieldWithSubFields | TabsField
|
||||
nestedFieldName2: string
|
||||
parentName: string
|
||||
}
|
||||
|
||||
export const recursivelyBuildNestedPaths = ({
|
||||
field,
|
||||
nestedFieldName2,
|
||||
parentName,
|
||||
}: RecursivelyBuildNestedPathsArgs) => {
|
||||
const fieldName = fieldAffectsData(field) ? field.name : undefined
|
||||
const nestedFieldName = fieldName || nestedFieldName2
|
||||
|
||||
if (field.type === 'tabs') {
|
||||
// if the tab has a name, treat it as a group
|
||||
// otherwise, treat it as a row
|
||||
return field.tabs.reduce((tabSchema, tab: any) => {
|
||||
tabSchema.push(
|
||||
...recursivelyBuildNestedPaths({
|
||||
field: {
|
||||
...tab,
|
||||
type: 'name' in tab ? 'group' : 'row',
|
||||
},
|
||||
nestedFieldName2: nestedFieldName,
|
||||
parentName,
|
||||
}),
|
||||
)
|
||||
return tabSchema
|
||||
}, [])
|
||||
}
|
||||
|
||||
const nestedPaths = field.fields.reduce((nestedFields, nestedField) => {
|
||||
if (!fieldIsPresentationalOnly(nestedField)) {
|
||||
if (!fieldAffectsData(nestedField)) {
|
||||
return [
|
||||
...nestedFields,
|
||||
...recursivelyBuildNestedPaths({
|
||||
field: nestedField,
|
||||
nestedFieldName2: nestedFieldName,
|
||||
parentName,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
const nestedPathName = fieldAffectsData(nestedField)
|
||||
? `${nestedFieldName ? `${nestedFieldName}__` : ''}${nestedField.name}`
|
||||
: undefined
|
||||
const getFieldSchema = fieldToSchemaMap({
|
||||
nestedFieldName,
|
||||
parentName,
|
||||
})[nestedField.type]
|
||||
|
||||
if (getFieldSchema) {
|
||||
const fieldSchema = getFieldSchema({
|
||||
...nestedField,
|
||||
name: nestedPathName,
|
||||
})
|
||||
|
||||
if (Array.isArray(fieldSchema)) {
|
||||
return [...nestedFields, ...fieldSchema]
|
||||
}
|
||||
|
||||
return [
|
||||
...nestedFields,
|
||||
{
|
||||
type: fieldSchema,
|
||||
key: nestedPathName,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return nestedFields
|
||||
}, [])
|
||||
|
||||
return nestedPaths
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import type { FieldWithSubFields, TabsField } from 'payload'
|
||||
|
||||
import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared'
|
||||
|
||||
import { fieldToSchemaMap } from './fieldToWhereInputSchemaMap.js'
|
||||
|
||||
type Args = {
|
||||
field: FieldWithSubFields | TabsField
|
||||
nestedFieldName2: string
|
||||
parentName: string
|
||||
}
|
||||
|
||||
export const recursivelyBuildNestedPaths = ({ field, nestedFieldName2, parentName }: Args) => {
|
||||
const fieldName = fieldAffectsData(field) ? field.name : undefined
|
||||
const nestedFieldName = fieldName || nestedFieldName2
|
||||
|
||||
if (field.type === 'tabs') {
|
||||
// if the tab has a name, treat it as a group
|
||||
// otherwise, treat it as a row
|
||||
return field.tabs.reduce((tabSchema, tab: any) => {
|
||||
tabSchema.push(
|
||||
...recursivelyBuildNestedPaths({
|
||||
field: {
|
||||
...tab,
|
||||
type: 'name' in tab ? 'group' : 'row',
|
||||
},
|
||||
nestedFieldName2: nestedFieldName,
|
||||
parentName,
|
||||
}),
|
||||
)
|
||||
return tabSchema
|
||||
}, [])
|
||||
}
|
||||
|
||||
const nestedPaths = field.fields.reduce((nestedFields, nestedField) => {
|
||||
if (!fieldIsPresentationalOnly(nestedField)) {
|
||||
if (!fieldAffectsData(nestedField)) {
|
||||
return [
|
||||
...nestedFields,
|
||||
...recursivelyBuildNestedPaths({
|
||||
field: nestedField,
|
||||
nestedFieldName2: nestedFieldName,
|
||||
parentName,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
const nestedPathName = fieldAffectsData(nestedField)
|
||||
? `${nestedFieldName ? `${nestedFieldName}__` : ''}${nestedField.name}`
|
||||
: undefined
|
||||
const getFieldSchema = fieldToSchemaMap({
|
||||
nestedFieldName,
|
||||
parentName,
|
||||
})[nestedField.type]
|
||||
|
||||
if (getFieldSchema) {
|
||||
const fieldSchema = getFieldSchema({
|
||||
...nestedField,
|
||||
name: nestedPathName,
|
||||
})
|
||||
|
||||
if (Array.isArray(fieldSchema)) {
|
||||
return [...nestedFields, ...fieldSchema]
|
||||
}
|
||||
|
||||
return [
|
||||
...nestedFields,
|
||||
{
|
||||
type: fieldSchema,
|
||||
key: nestedPathName,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return nestedFields
|
||||
}, [])
|
||||
|
||||
return nestedPaths
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import type { CollectionConfig } from '../../index.js'
|
||||
|
||||
import { InvalidConfiguration } from '../../errors/InvalidConfiguration.js'
|
||||
import { fieldAffectsData, fieldIsVirtual } from '../../fields/config/types.js'
|
||||
import flattenFields from '../../utilities/flattenTopLevelFields.js'
|
||||
import { flattenTopLevelFields } from '../../utilities/flattenTopLevelFields.js'
|
||||
|
||||
/**
|
||||
* Validate useAsTitle for collections.
|
||||
@@ -15,7 +15,7 @@ export const validateUseAsTitle = (config: CollectionConfig) => {
|
||||
}
|
||||
|
||||
if (config?.admin && config.admin?.useAsTitle && config.admin.useAsTitle !== 'id') {
|
||||
const fields = flattenFields(config.fields)
|
||||
const fields = flattenTopLevelFields(config.fields)
|
||||
const useAsTitleField = fields.find((field) => {
|
||||
if (fieldAffectsData(field) && config.admin) {
|
||||
return field.name === config.admin.useAsTitle
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Collection } from '../config/types.js'
|
||||
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { combineQueries } from '../../database/combineQueries.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateSearchParams.js'
|
||||
import { killTransaction } from '../../utilities/killTransaction.js'
|
||||
import { buildAfterOperation } from './utils.js'
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { AccessResult } from '../../config/types.js'
|
||||
import type { CollectionSlug } from '../../index.js'
|
||||
import type { PayloadRequest, Where } from '../../types/index.js'
|
||||
import type { Collection } from '../config/types.js'
|
||||
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { combineQueries } from '../../database/combineQueries.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths.js'
|
||||
import { buildVersionCollectionFields, type CollectionSlug } from '../../index.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateSearchParams.js'
|
||||
import { killTransaction } from '../../utilities/killTransaction.js'
|
||||
import { buildVersionCollectionFields } from '../../versions/buildCollectionFields.js'
|
||||
import { buildAfterOperation } from './utils.js'
|
||||
|
||||
export type Arguments = {
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { combineQueries } from '../../database/combineQueries.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateSearchParams.js'
|
||||
import { APIError } from '../../errors/index.js'
|
||||
import { afterRead } from '../../fields/hooks/afterRead/index.js'
|
||||
import { deleteUserPreferences } from '../../preferences/deleteUserPreferences.js'
|
||||
|
||||
@@ -17,7 +17,7 @@ import type {
|
||||
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { combineQueries } from '../../database/combineQueries.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateSearchParams.js'
|
||||
import { sanitizeJoinQuery } from '../../database/sanitizeJoinQuery.js'
|
||||
import { afterRead } from '../../fields/hooks/afterRead/index.js'
|
||||
import { killTransaction } from '../../utilities/killTransaction.js'
|
||||
|
||||
@@ -15,10 +15,10 @@ import type {
|
||||
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { combineQueries } from '../../database/combineQueries.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateSearchParams.js'
|
||||
import { sanitizeJoinQuery } from '../../database/sanitizeJoinQuery.js'
|
||||
import { NotFound } from '../../errors/index.js'
|
||||
import { NotFound } from '../../errors/NotFound.js'
|
||||
import { afterRead } from '../../fields/hooks/afterRead/index.js'
|
||||
import { validateQueryPaths } from '../../index.js'
|
||||
import { killTransaction } from '../../utilities/killTransaction.js'
|
||||
import replaceWithDraftIfAvailable from '../../versions/drafts/replaceWithDraftIfAvailable.js'
|
||||
import { buildAfterOperation } from './utils.js'
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Collection } from '../config/types.js'
|
||||
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { combineQueries } from '../../database/combineQueries.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateSearchParams.js'
|
||||
import { afterRead } from '../../fields/hooks/afterRead/index.js'
|
||||
import { killTransaction } from '../../utilities/killTransaction.js'
|
||||
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields.js'
|
||||
|
||||
@@ -16,8 +16,8 @@ import type {
|
||||
import { ensureUsernameOrEmail } from '../../auth/ensureUsernameOrEmail.js'
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { combineQueries } from '../../database/combineQueries.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths.js'
|
||||
import { APIError } from '../../errors/index.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateSearchParams.js'
|
||||
import { APIError } from '../../errors/APIError.js'
|
||||
import { afterChange } from '../../fields/hooks/afterChange/index.js'
|
||||
import { afterRead } from '../../fields/hooks/afterRead/index.js'
|
||||
import { beforeChange } from '../../fields/hooks/beforeChange/index.js'
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Payload } from '../index.js'
|
||||
import type { PathToQuery } from './queryValidation/types.js'
|
||||
|
||||
import { fieldAffectsData } from '../fields/config/types.js'
|
||||
import flattenFields from '../utilities/flattenTopLevelFields.js'
|
||||
import { flattenTopLevelFields } from '../utilities/flattenTopLevelFields.js'
|
||||
|
||||
export async function getLocalizedPaths({
|
||||
collectionSlug,
|
||||
@@ -30,7 +30,7 @@ export async function getLocalizedPaths({
|
||||
collectionSlug,
|
||||
complete: false,
|
||||
field: undefined,
|
||||
fields: flattenFields(fields, false),
|
||||
fields: flattenTopLevelFields(fields, false),
|
||||
globalSlug,
|
||||
invalid: false,
|
||||
path: '',
|
||||
@@ -151,7 +151,10 @@ export async function getLocalizedPaths({
|
||||
|
||||
default: {
|
||||
if ('fields' in lastIncompletePath.field) {
|
||||
lastIncompletePath.fields = flattenFields(lastIncompletePath.field.fields, false)
|
||||
lastIncompletePath.fields = flattenTopLevelFields(
|
||||
lastIncompletePath.field.fields,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
if (i + 1 === pathSegments.length) {
|
||||
|
||||
@@ -2,8 +2,8 @@ import fs from 'fs'
|
||||
|
||||
import type { CreateMigration } from '../types.js'
|
||||
|
||||
import { writeMigrationIndex } from '../../index.js'
|
||||
import { migrationTemplate } from './migrationTemplate.js'
|
||||
import { writeMigrationIndex } from './writeMigrationIndex.js'
|
||||
|
||||
export const createMigration: CreateMigration = function createMigration({
|
||||
migrationName,
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'
|
||||
import type { Field, FieldAffectingData } from '../../fields/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
|
||||
import type { Operator, PayloadRequest, Where, WhereField } from '../../types/index.js'
|
||||
import type { EntityPolicies } from './types.js'
|
||||
|
||||
import { QueryError } from '../../errors/QueryError.js'
|
||||
import { validOperators } from '../../types/constants.js'
|
||||
import { deepCopyObject } from '../../utilities/deepCopyObject.js'
|
||||
import flattenFields from '../../utilities/flattenTopLevelFields.js'
|
||||
import { validateSearchParam } from './validateSearchParams.js'
|
||||
|
||||
type Args = {
|
||||
errors?: { path: string }[]
|
||||
overrideAccess: boolean
|
||||
policies?: EntityPolicies
|
||||
req: PayloadRequest
|
||||
versionFields?: Field[]
|
||||
where: Where
|
||||
} & (
|
||||
| {
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
globalConfig?: never | undefined
|
||||
}
|
||||
| {
|
||||
collectionConfig?: never | undefined
|
||||
globalConfig: SanitizedGlobalConfig
|
||||
}
|
||||
)
|
||||
|
||||
const flattenWhere = (query: Where): WhereField[] =>
|
||||
Object.entries(query).reduce((flattenedConstraints, [key, val]) => {
|
||||
if ((key === 'and' || key === 'or') && Array.isArray(val)) {
|
||||
const subWhereConstraints: Where[] = val.reduce((acc, subVal) => {
|
||||
const subWhere = flattenWhere(subVal)
|
||||
return [...acc, ...subWhere]
|
||||
}, [])
|
||||
return [...flattenedConstraints, ...subWhereConstraints]
|
||||
}
|
||||
|
||||
return [...flattenedConstraints, { [key]: val }]
|
||||
}, [])
|
||||
|
||||
export async function validateQueryPaths({
|
||||
collectionConfig,
|
||||
errors = [],
|
||||
globalConfig,
|
||||
overrideAccess,
|
||||
policies = {
|
||||
collections: {},
|
||||
globals: {},
|
||||
},
|
||||
req,
|
||||
versionFields,
|
||||
where,
|
||||
}: Args): Promise<void> {
|
||||
const fields = flattenFields(
|
||||
versionFields || (globalConfig || collectionConfig).fields,
|
||||
) as FieldAffectingData[]
|
||||
if (typeof where === 'object') {
|
||||
const whereFields = flattenWhere(where)
|
||||
// We need to determine if the whereKey is an AND, OR, or a schema path
|
||||
const promises = []
|
||||
void whereFields.map((constraint) => {
|
||||
void Object.keys(constraint).map((path) => {
|
||||
void Object.entries(constraint[path]).map(([operator, val]) => {
|
||||
if (validOperators.includes(operator as Operator)) {
|
||||
promises.push(
|
||||
validateSearchParam({
|
||||
collectionConfig: deepCopyObject(collectionConfig),
|
||||
errors,
|
||||
fields: fields as Field[],
|
||||
globalConfig: deepCopyObject(globalConfig),
|
||||
operator,
|
||||
overrideAccess,
|
||||
path,
|
||||
policies,
|
||||
req,
|
||||
val,
|
||||
versionFields,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
await Promise.all(promises)
|
||||
if (errors.length > 0) {
|
||||
throw new QueryError(errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'
|
||||
import type { Field } from '../../fields/config/types.js'
|
||||
import type { Field, FieldAffectingData } from '../../fields/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
|
||||
import type { PayloadRequest } from '../../types/index.js'
|
||||
import type { Operator, PayloadRequest, Where, WhereField } from '../../types/index.js'
|
||||
import type { EntityPolicies, PathToQuery } from './types.js'
|
||||
|
||||
import { QueryError } from '../../errors/QueryError.js'
|
||||
import { fieldAffectsData, fieldIsVirtual } from '../../fields/config/types.js'
|
||||
import { validOperators } from '../../types/constants.js'
|
||||
import { deepCopyObject } from '../../utilities/deepCopyObject.js'
|
||||
import { flattenTopLevelFields } from '../../utilities/flattenTopLevelFields.js'
|
||||
import { getEntityPolicies } from '../../utilities/getEntityPolicies.js'
|
||||
import isolateObjectProperty from '../../utilities/isolateObjectProperty.js'
|
||||
import { getLocalizedPaths } from '../getLocalizedPaths.js'
|
||||
import { validateQueryPaths } from './validateQueryPaths.js'
|
||||
|
||||
type Args = {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
@@ -193,3 +196,84 @@ export async function validateSearchParam({
|
||||
)
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
type ValidateQueryPathsArgs = {
|
||||
errors?: { path: string }[]
|
||||
overrideAccess: boolean
|
||||
policies?: EntityPolicies
|
||||
req: PayloadRequest
|
||||
versionFields?: Field[]
|
||||
where: Where
|
||||
} & (
|
||||
| {
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
globalConfig?: never | undefined
|
||||
}
|
||||
| {
|
||||
collectionConfig?: never | undefined
|
||||
globalConfig: SanitizedGlobalConfig
|
||||
}
|
||||
)
|
||||
|
||||
const flattenWhere = (query: Where): WhereField[] =>
|
||||
Object.entries(query).reduce((flattenedConstraints, [key, val]) => {
|
||||
if ((key === 'and' || key === 'or') && Array.isArray(val)) {
|
||||
const subWhereConstraints: Where[] = val.reduce((acc, subVal) => {
|
||||
const subWhere = flattenWhere(subVal)
|
||||
return [...acc, ...subWhere]
|
||||
}, [])
|
||||
return [...flattenedConstraints, ...subWhereConstraints]
|
||||
}
|
||||
|
||||
return [...flattenedConstraints, { [key]: val }]
|
||||
}, [])
|
||||
|
||||
export async function validateQueryPaths({
|
||||
collectionConfig,
|
||||
errors = [],
|
||||
globalConfig,
|
||||
overrideAccess,
|
||||
policies = {
|
||||
collections: {},
|
||||
globals: {},
|
||||
},
|
||||
req,
|
||||
versionFields,
|
||||
where,
|
||||
}: ValidateQueryPathsArgs): Promise<void> {
|
||||
const fields = flattenTopLevelFields(
|
||||
versionFields || (globalConfig || collectionConfig).fields,
|
||||
) as FieldAffectingData[]
|
||||
if (typeof where === 'object') {
|
||||
const whereFields = flattenWhere(where)
|
||||
// We need to determine if the whereKey is an AND, OR, or a schema path
|
||||
const promises = []
|
||||
void whereFields.map((constraint) => {
|
||||
void Object.keys(constraint).map((path) => {
|
||||
void Object.entries(constraint[path]).map(([operator, val]) => {
|
||||
if (validOperators.includes(operator as Operator)) {
|
||||
promises.push(
|
||||
validateSearchParam({
|
||||
collectionConfig: deepCopyObject(collectionConfig),
|
||||
errors,
|
||||
fields: fields as Field[],
|
||||
globalConfig: deepCopyObject(globalConfig),
|
||||
operator,
|
||||
overrideAccess,
|
||||
path,
|
||||
policies,
|
||||
req,
|
||||
val,
|
||||
versionFields,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
await Promise.all(promises)
|
||||
if (errors.length > 0) {
|
||||
throw new QueryError(errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { JoinQuery, PayloadRequest } from '../types/index.js'
|
||||
import executeAccess from '../auth/executeAccess.js'
|
||||
import { QueryError } from '../errors/QueryError.js'
|
||||
import { combineQueries } from './combineQueries.js'
|
||||
import { validateQueryPaths } from './queryValidation/validateQueryPaths.js'
|
||||
import { validateQueryPaths } from './queryValidation/validateSearchParams.js'
|
||||
|
||||
type Args = {
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
|
||||
@@ -14,17 +14,17 @@ import type {
|
||||
SelectFieldClient,
|
||||
TabsFieldClient,
|
||||
} from '../../fields/config/types.js'
|
||||
import type { ImportMap } from '../../index.js'
|
||||
import type { Payload } from '../../types/index.js'
|
||||
|
||||
import { getFromImportMap } from '../../bin/generateImportMap/getFromImportMap.js'
|
||||
import { MissingEditorProp } from '../../errors/MissingEditorProp.js'
|
||||
import { fieldAffectsData } from '../../fields/config/types.js'
|
||||
import { flattenTopLevelFields, type ImportMap } from '../../index.js'
|
||||
import { flattenTopLevelFields } from '../../utilities/flattenTopLevelFields.js'
|
||||
import { removeUndefined } from '../../utilities/removeUndefined.js'
|
||||
|
||||
// Should not be used - ClientField should be used instead. This is why we don't export ClientField, we don't want people
|
||||
// to accidentally use it instead of ClientField and get confused
|
||||
|
||||
export { ClientField }
|
||||
|
||||
export type ServerOnlyFieldProperties =
|
||||
|
||||
@@ -1,305 +0,0 @@
|
||||
import type { RichTextAdapter } from '../../../admin/RichText.js'
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { RequestContext } from '../../../index.js'
|
||||
import type { JsonObject, PayloadRequest } from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args = {
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: JsonObject
|
||||
doc: JsonObject
|
||||
field: Field | TabAsField
|
||||
fieldIndex: number
|
||||
global: null | SanitizedGlobalConfig
|
||||
operation: 'create' | 'update'
|
||||
/**
|
||||
* The parent's path
|
||||
*/
|
||||
parentPath: (number | string)[]
|
||||
/**
|
||||
* The parent's schemaPath (path without indexes).
|
||||
*/
|
||||
parentSchemaPath: string[]
|
||||
previousDoc: JsonObject
|
||||
previousSiblingDoc: JsonObject
|
||||
req: PayloadRequest
|
||||
siblingData: JsonObject
|
||||
siblingDoc: JsonObject
|
||||
}
|
||||
|
||||
// This function is responsible for the following actions, in order:
|
||||
// - Execute field hooks
|
||||
|
||||
export const promise = async ({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
field,
|
||||
fieldIndex,
|
||||
global,
|
||||
operation,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}: Args): Promise<void> => {
|
||||
const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data
|
||||
parentPath: parentPath.join('.'),
|
||||
parentSchemaPath: parentSchemaPath.join('.'),
|
||||
})
|
||||
const fieldPath = _fieldPath ? _fieldPath.split('.') : []
|
||||
const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : []
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
// Execute hooks
|
||||
if (field.hooks?.afterChange) {
|
||||
await field.hooks.afterChange.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
field,
|
||||
global,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
previousValue: previousDoc[field.name],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
value: siblingDoc[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name] = hookedValue
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse subfields
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
const rows = siblingDoc[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
path: [...fieldPath, i],
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as JsonObject),
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData?.[field.name]?.[i] || {},
|
||||
siblingDoc: row ? { ...row } : {},
|
||||
}),
|
||||
)
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const rows = siblingDoc[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
const block = field.blocks.find(
|
||||
(blockType) => blockType.slug === (row as JsonObject).blockType,
|
||||
)
|
||||
|
||||
if (block) {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: block.fields,
|
||||
global,
|
||||
operation,
|
||||
path: [...fieldPath, i],
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as JsonObject),
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData?.[field.name]?.[i] || {},
|
||||
siblingDoc: row ? { ...row } : {},
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'collapsible':
|
||||
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: { ...previousSiblingDoc },
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData || {},
|
||||
siblingDoc: { ...siblingDoc },
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
case 'group': {
|
||||
await traverseFields({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc[field.name] as JsonObject,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: (siblingData?.[field.name] as JsonObject) || {},
|
||||
siblingDoc: siblingDoc[field.name] as JsonObject,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
if (editor?.hooks?.afterChange?.length) {
|
||||
await editor.hooks.afterChange.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
field,
|
||||
global,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
previousValue: previousDoc[field.name],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
value: siblingDoc[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name] = hookedValue
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'tab': {
|
||||
let tabSiblingData = siblingData
|
||||
let tabSiblingDoc = siblingDoc
|
||||
let tabPreviousSiblingDoc = siblingDoc
|
||||
|
||||
if (tabHasName(field)) {
|
||||
tabSiblingData = siblingData[field.name] as JsonObject
|
||||
tabSiblingDoc = siblingDoc[field.name] as JsonObject
|
||||
tabPreviousSiblingDoc = previousDoc[field.name] as JsonObject
|
||||
}
|
||||
|
||||
await traverseFields({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: tabPreviousSiblingDoc,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: tabSiblingData,
|
||||
siblingDoc: tabSiblingDoc,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
global,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: { ...previousSiblingDoc },
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData || {},
|
||||
siblingDoc: { ...siblingDoc },
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import type { RichTextAdapter } from '../../../admin/RichText.js'
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { RequestContext } from '../../../index.js'
|
||||
import type { JsonObject, PayloadRequest } from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { promise } from './promise.js'
|
||||
import { MissingEditorProp } from '../../../errors/MissingEditorProp.js'
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
|
||||
type Args = {
|
||||
collection: null | SanitizedCollectionConfig
|
||||
@@ -65,3 +68,296 @@ export const traverseFields = async ({
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
type PromiseArgs = {
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: JsonObject
|
||||
doc: JsonObject
|
||||
field: Field | TabAsField
|
||||
fieldIndex: number
|
||||
global: null | SanitizedGlobalConfig
|
||||
operation: 'create' | 'update'
|
||||
/**
|
||||
* The parent's path
|
||||
*/
|
||||
parentPath: (number | string)[]
|
||||
/**
|
||||
* The parent's schemaPath (path without indexes).
|
||||
*/
|
||||
parentSchemaPath: string[]
|
||||
previousDoc: JsonObject
|
||||
previousSiblingDoc: JsonObject
|
||||
req: PayloadRequest
|
||||
siblingData: JsonObject
|
||||
siblingDoc: JsonObject
|
||||
}
|
||||
|
||||
// This function is responsible for the following actions, in order:
|
||||
// - Execute field hooks
|
||||
|
||||
export const promise = async ({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
field,
|
||||
fieldIndex,
|
||||
global,
|
||||
operation,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}: PromiseArgs): Promise<void> => {
|
||||
const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data
|
||||
parentPath: parentPath.join('.'),
|
||||
parentSchemaPath: parentSchemaPath.join('.'),
|
||||
})
|
||||
const fieldPath = _fieldPath ? _fieldPath.split('.') : []
|
||||
const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : []
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
// Execute hooks
|
||||
if (field.hooks?.afterChange) {
|
||||
await field.hooks.afterChange.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
field,
|
||||
global,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
previousValue: previousDoc[field.name],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
value: siblingDoc[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name] = hookedValue
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse subfields
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
const rows = siblingDoc[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
path: [...fieldPath, i],
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as JsonObject),
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData?.[field.name]?.[i] || {},
|
||||
siblingDoc: row ? { ...row } : {},
|
||||
}),
|
||||
)
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const rows = siblingDoc[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
const block = field.blocks.find(
|
||||
(blockType) => blockType.slug === (row as JsonObject).blockType,
|
||||
)
|
||||
|
||||
if (block) {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: block.fields,
|
||||
global,
|
||||
operation,
|
||||
path: [...fieldPath, i],
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as JsonObject),
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData?.[field.name]?.[i] || {},
|
||||
siblingDoc: row ? { ...row } : {},
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: { ...previousSiblingDoc },
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData || {},
|
||||
siblingDoc: { ...siblingDoc },
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
case 'group': {
|
||||
await traverseFields({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc[field.name] as JsonObject,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: (siblingData?.[field.name] as JsonObject) || {},
|
||||
siblingDoc: siblingDoc[field.name] as JsonObject,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
if (editor?.hooks?.afterChange?.length) {
|
||||
await editor.hooks.afterChange.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
field,
|
||||
global,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
previousValue: previousDoc[field.name],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
value: siblingDoc[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name] = hookedValue
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'tab': {
|
||||
let tabSiblingData = siblingData
|
||||
let tabSiblingDoc = siblingDoc
|
||||
let tabPreviousSiblingDoc = siblingDoc
|
||||
|
||||
if (tabHasName(field)) {
|
||||
tabSiblingData = siblingData[field.name] as JsonObject
|
||||
tabSiblingDoc = siblingDoc[field.name] as JsonObject
|
||||
tabPreviousSiblingDoc = previousDoc[field.name] as JsonObject
|
||||
}
|
||||
|
||||
await traverseFields({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: tabPreviousSiblingDoc,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: tabSiblingData,
|
||||
siblingDoc: tabSiblingDoc,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
global,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: { ...previousSiblingDoc },
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData || {},
|
||||
siblingDoc: { ...siblingDoc },
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,788 +0,0 @@
|
||||
import type { RichTextAdapter } from '../../../admin/RichText.js'
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { RequestContext } from '../../../index.js'
|
||||
import type {
|
||||
JsonObject,
|
||||
PayloadRequest,
|
||||
PopulateType,
|
||||
SelectMode,
|
||||
SelectType,
|
||||
} from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
import { getDefaultValue } from '../../getDefaultValue.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { relationshipPopulationPromise } from './relationshipPopulationPromise.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args = {
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
currentDepth: number
|
||||
depth: number
|
||||
doc: JsonObject
|
||||
draft: boolean
|
||||
fallbackLocale: null | string
|
||||
field: Field | TabAsField
|
||||
fieldIndex: number
|
||||
/**
|
||||
* fieldPromises are used for things like field hooks. They should be awaited before awaiting populationPromises
|
||||
*/
|
||||
fieldPromises: Promise<void>[]
|
||||
findMany: boolean
|
||||
flattenLocales: boolean
|
||||
global: null | SanitizedGlobalConfig
|
||||
locale: null | string
|
||||
overrideAccess: boolean
|
||||
/**
|
||||
* The parent's path.
|
||||
*/
|
||||
parentPath: (number | string)[]
|
||||
/**
|
||||
* The parent's schemaPath (path without indexes).
|
||||
*/
|
||||
parentSchemaPath: string[]
|
||||
populate?: PopulateType
|
||||
populationPromises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
select?: SelectType
|
||||
selectMode?: SelectMode
|
||||
showHiddenFields: boolean
|
||||
siblingDoc: JsonObject
|
||||
triggerAccessControl?: boolean
|
||||
triggerHooks?: boolean
|
||||
}
|
||||
|
||||
// This function is responsible for the following actions, in order:
|
||||
// - Remove hidden fields from response
|
||||
// - Flatten locales into requested locale
|
||||
// - Sanitize outgoing data (point field, etc.)
|
||||
// - Execute field hooks
|
||||
// - Execute read access control
|
||||
// - Populate relationships
|
||||
|
||||
export const promise = async ({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
field,
|
||||
fieldIndex,
|
||||
fieldPromises,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
select,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
triggerAccessControl = true,
|
||||
triggerHooks = true,
|
||||
}: Args): Promise<void> => {
|
||||
const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data
|
||||
parentPath: parentPath.join('.'),
|
||||
parentSchemaPath: parentSchemaPath.join('.'),
|
||||
})
|
||||
const fieldPath = _fieldPath ? _fieldPath.split('.') : []
|
||||
const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : []
|
||||
|
||||
if (
|
||||
fieldAffectsData(field) &&
|
||||
field.hidden &&
|
||||
typeof siblingDoc[field.name] !== 'undefined' &&
|
||||
!showHiddenFields
|
||||
) {
|
||||
delete siblingDoc[field.name]
|
||||
}
|
||||
|
||||
if (fieldAffectsData(field) && select && selectMode) {
|
||||
if (selectMode === 'include') {
|
||||
if (!select[field.name]) {
|
||||
delete siblingDoc[field.name]
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (selectMode === 'exclude') {
|
||||
if (select[field.name] === false) {
|
||||
delete siblingDoc[field.name]
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shouldHoistLocalizedValue =
|
||||
flattenLocales &&
|
||||
fieldAffectsData(field) &&
|
||||
typeof siblingDoc[field.name] === 'object' &&
|
||||
siblingDoc[field.name] !== null &&
|
||||
field.localized &&
|
||||
locale !== 'all' &&
|
||||
req.payload.config.localization
|
||||
|
||||
if (shouldHoistLocalizedValue) {
|
||||
// replace actual value with localized value before sanitizing
|
||||
// { [locale]: fields } -> fields
|
||||
const value = siblingDoc[field.name][locale]
|
||||
|
||||
let hoistedValue = value
|
||||
|
||||
if (fallbackLocale && fallbackLocale !== locale) {
|
||||
const fallbackValue = siblingDoc[field.name][fallbackLocale]
|
||||
const isNullOrUndefined = typeof value === 'undefined' || value === null
|
||||
|
||||
if (fallbackValue) {
|
||||
switch (field.type) {
|
||||
case 'text':
|
||||
case 'textarea': {
|
||||
if (value === '' || isNullOrUndefined) {
|
||||
hoistedValue = fallbackValue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
if (isNullOrUndefined) {
|
||||
hoistedValue = fallbackValue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
siblingDoc[field.name] = hoistedValue
|
||||
}
|
||||
|
||||
// Sanitize outgoing field value
|
||||
switch (field.type) {
|
||||
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') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'point': {
|
||||
const pointDoc = siblingDoc[field.name] as Record<string, unknown>
|
||||
if (Array.isArray(pointDoc?.coordinates) && pointDoc.coordinates.length === 2) {
|
||||
siblingDoc[field.name] = pointDoc.coordinates
|
||||
} else {
|
||||
siblingDoc[field.name] = undefined
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
// Rich Text fields should use afterRead hooks to do population. The previous editor.populationPromises have been renamed to editor.graphQLPopulationPromises
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
field.tabs.forEach((tab) => {
|
||||
if (
|
||||
tabHasName(tab) &&
|
||||
(typeof siblingDoc[tab.name] === 'undefined' || siblingDoc[tab.name] === null)
|
||||
) {
|
||||
siblingDoc[tab.name] = {}
|
||||
}
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
// Execute hooks
|
||||
if (triggerHooks && field.hooks?.afterRead) {
|
||||
await field.hooks.afterRead.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const shouldRunHookOnAllLocales =
|
||||
field.localized &&
|
||||
(locale === 'all' || !flattenLocales) &&
|
||||
typeof siblingDoc[field.name] === 'object'
|
||||
|
||||
if (shouldRunHookOnAllLocales) {
|
||||
const hookPromises = Object.entries(siblingDoc[field.name]).map(([locale, value]) =>
|
||||
(async () => {
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
data: doc,
|
||||
depth,
|
||||
draft,
|
||||
field,
|
||||
findMany,
|
||||
global,
|
||||
operation: 'read',
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingData: siblingDoc,
|
||||
value,
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name][locale] = hookedValue
|
||||
}
|
||||
})(),
|
||||
)
|
||||
|
||||
await Promise.all(hookPromises)
|
||||
} else {
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
data: doc,
|
||||
depth,
|
||||
draft,
|
||||
field,
|
||||
findMany,
|
||||
global,
|
||||
operation: 'read',
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingData: siblingDoc,
|
||||
value: siblingDoc[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name] = hookedValue
|
||||
}
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
|
||||
// Execute access control
|
||||
let allowDefaultValue = true
|
||||
if (triggerAccessControl && field.access && field.access.read) {
|
||||
const result = overrideAccess
|
||||
? true
|
||||
: await field.access.read({
|
||||
id: doc.id as number | string,
|
||||
data: doc,
|
||||
doc,
|
||||
req,
|
||||
siblingData: siblingDoc,
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
allowDefaultValue = false
|
||||
delete siblingDoc[field.name]
|
||||
}
|
||||
}
|
||||
|
||||
// Set defaultValue on the field for globals being returned without being first created
|
||||
// or collection documents created prior to having a default
|
||||
if (
|
||||
allowDefaultValue &&
|
||||
typeof siblingDoc[field.name] === 'undefined' &&
|
||||
typeof field.defaultValue !== 'undefined'
|
||||
) {
|
||||
siblingDoc[field.name] = await getDefaultValue({
|
||||
defaultValue: field.defaultValue,
|
||||
locale,
|
||||
user: req.user,
|
||||
value: siblingDoc[field.name],
|
||||
})
|
||||
}
|
||||
|
||||
if (field.type === 'relationship' || field.type === 'upload' || field.type === 'join') {
|
||||
populationPromises.push(
|
||||
relationshipPopulationPromise({
|
||||
currentDepth,
|
||||
depth,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
field,
|
||||
locale,
|
||||
overrideAccess,
|
||||
populate,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
const rows = siblingDoc[field.name] as JsonObject
|
||||
|
||||
const arraySelect = select?.[field.name]
|
||||
|
||||
if (selectMode === 'include' && typeof arraySelect === 'object') {
|
||||
arraySelect.id = true
|
||||
}
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, i) => {
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: field.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select: typeof arraySelect === 'object' ? arraySelect : undefined,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
siblingDoc: row || {},
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
})
|
||||
} else if (!shouldHoistLocalizedValue && typeof rows === 'object' && rows !== null) {
|
||||
Object.values(rows).forEach((localeRows) => {
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row, i) => {
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: field.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingDoc: (row as JsonObject) || {},
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
siblingDoc[field.name] = []
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const rows = siblingDoc[field.name]
|
||||
|
||||
const blocksSelect = select?.[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, i) => {
|
||||
const block = field.blocks.find(
|
||||
(blockType) => blockType.slug === (row as JsonObject).blockType,
|
||||
)
|
||||
|
||||
let blockSelectMode = selectMode
|
||||
|
||||
if (typeof blocksSelect === 'object') {
|
||||
// sanitize blocks: {cta: false} to blocks: {cta: {id: true, blockType: true}}
|
||||
if (selectMode === 'exclude' && blocksSelect[block.slug] === false) {
|
||||
blockSelectMode = 'include'
|
||||
blocksSelect[block.slug] = {
|
||||
id: true,
|
||||
blockType: true,
|
||||
}
|
||||
} else if (selectMode === 'include') {
|
||||
if (!blocksSelect[block.slug]) {
|
||||
blocksSelect[block.slug] = {}
|
||||
}
|
||||
|
||||
if (typeof blocksSelect[block.slug] === 'object') {
|
||||
blocksSelect[block.slug]['id'] = true
|
||||
blocksSelect[block.slug]['blockType'] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const blockSelect = blocksSelect?.[block.slug]
|
||||
|
||||
if (block) {
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: block.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select: typeof blockSelect === 'object' ? blockSelect : undefined,
|
||||
selectMode: blockSelectMode,
|
||||
showHiddenFields,
|
||||
siblingDoc: (row as JsonObject) || {},
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (!shouldHoistLocalizedValue && typeof rows === 'object' && rows !== null) {
|
||||
Object.values(rows).forEach((localeRows) => {
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row, i) => {
|
||||
const block = field.blocks.find(
|
||||
(blockType) => blockType.slug === (row as JsonObject).blockType,
|
||||
)
|
||||
|
||||
if (block) {
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: block.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingDoc: (row as JsonObject) || {},
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
siblingDoc[field.name] = []
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'collapsible':
|
||||
|
||||
case 'row': {
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: field.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
case 'group': {
|
||||
let groupDoc = siblingDoc[field.name] as JsonObject
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
groupDoc = {}
|
||||
}
|
||||
|
||||
const groupSelect = select?.[field.name]
|
||||
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: field.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select: typeof groupSelect === 'object' ? groupSelect : undefined,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
siblingDoc: groupDoc,
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
if (editor?.hooks?.afterRead?.length) {
|
||||
await editor.hooks.afterRead.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const shouldRunHookOnAllLocales =
|
||||
field.localized &&
|
||||
(locale === 'all' || !flattenLocales) &&
|
||||
typeof siblingDoc[field.name] === 'object'
|
||||
|
||||
if (shouldRunHookOnAllLocales) {
|
||||
const hookPromises = Object.entries(siblingDoc[field.name]).map(([locale, value]) =>
|
||||
(async () => {
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
data: doc,
|
||||
depth,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
field,
|
||||
fieldPromises,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
operation: 'read',
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingData: siblingDoc,
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
value,
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name][locale] = hookedValue
|
||||
}
|
||||
})(),
|
||||
)
|
||||
|
||||
await Promise.all(hookPromises)
|
||||
} else {
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
data: doc,
|
||||
depth,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
field,
|
||||
fieldPromises,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
operation: 'read',
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingData: siblingDoc,
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
value: siblingDoc[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name] = hookedValue
|
||||
}
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'tab': {
|
||||
let tabDoc = siblingDoc
|
||||
let tabSelect: SelectType | undefined
|
||||
if (tabHasName(field)) {
|
||||
tabDoc = siblingDoc[field.name] as JsonObject
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
tabDoc = {}
|
||||
}
|
||||
|
||||
if (typeof select?.[field.name] === 'object') {
|
||||
tabSelect = select?.[field.name] as SelectType
|
||||
}
|
||||
} else {
|
||||
tabSelect = select
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: field.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select: tabSelect,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
siblingDoc: tabDoc,
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RichTextAdapter } from '../../../admin/RichText.js'
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { RequestContext } from '../../../index.js'
|
||||
@@ -10,7 +11,11 @@ import type {
|
||||
} from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { promise } from './promise.js'
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
import { getDefaultValue } from '../../getDefaultValue.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { relationshipPopulationPromise } from './relationshipPopulationPromise.js'
|
||||
|
||||
type Args = {
|
||||
collection: null | SanitizedCollectionConfig
|
||||
@@ -103,3 +108,772 @@ export const traverseFields = ({
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
type PromiseArgs = {
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
currentDepth: number
|
||||
depth: number
|
||||
doc: JsonObject
|
||||
draft: boolean
|
||||
fallbackLocale: null | string
|
||||
field: Field | TabAsField
|
||||
fieldIndex: number
|
||||
/**
|
||||
* fieldPromises are used for things like field hooks. They should be awaited before awaiting populationPromises
|
||||
*/
|
||||
fieldPromises: Promise<void>[]
|
||||
findMany: boolean
|
||||
flattenLocales: boolean
|
||||
global: null | SanitizedGlobalConfig
|
||||
locale: null | string
|
||||
overrideAccess: boolean
|
||||
/**
|
||||
* The parent's path.
|
||||
*/
|
||||
parentPath: (number | string)[]
|
||||
/**
|
||||
* The parent's schemaPath (path without indexes).
|
||||
*/
|
||||
parentSchemaPath: string[]
|
||||
populate?: PopulateType
|
||||
populationPromises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
select?: SelectType
|
||||
selectMode?: SelectMode
|
||||
showHiddenFields: boolean
|
||||
siblingDoc: JsonObject
|
||||
triggerAccessControl?: boolean
|
||||
triggerHooks?: boolean
|
||||
}
|
||||
|
||||
// This function is responsible for the following actions, in order:
|
||||
// - Remove hidden fields from response
|
||||
// - Flatten locales into requested locale
|
||||
// - Sanitize outgoing data (point field, etc.)
|
||||
// - Execute field hooks
|
||||
// - Execute read access control
|
||||
// - Populate relationships
|
||||
|
||||
export const promise = async ({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
field,
|
||||
fieldIndex,
|
||||
fieldPromises,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
select,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
triggerAccessControl = true,
|
||||
triggerHooks = true,
|
||||
}: PromiseArgs): Promise<void> => {
|
||||
const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data
|
||||
parentPath: parentPath.join('.'),
|
||||
parentSchemaPath: parentSchemaPath.join('.'),
|
||||
})
|
||||
const fieldPath = _fieldPath ? _fieldPath.split('.') : []
|
||||
const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : []
|
||||
|
||||
if (
|
||||
fieldAffectsData(field) &&
|
||||
field.hidden &&
|
||||
typeof siblingDoc[field.name] !== 'undefined' &&
|
||||
!showHiddenFields
|
||||
) {
|
||||
delete siblingDoc[field.name]
|
||||
}
|
||||
|
||||
if (fieldAffectsData(field) && select && selectMode) {
|
||||
if (selectMode === 'include') {
|
||||
if (!select[field.name]) {
|
||||
delete siblingDoc[field.name]
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (selectMode === 'exclude') {
|
||||
if (select[field.name] === false) {
|
||||
delete siblingDoc[field.name]
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shouldHoistLocalizedValue =
|
||||
flattenLocales &&
|
||||
fieldAffectsData(field) &&
|
||||
typeof siblingDoc[field.name] === 'object' &&
|
||||
siblingDoc[field.name] !== null &&
|
||||
field.localized &&
|
||||
locale !== 'all' &&
|
||||
req.payload.config.localization
|
||||
|
||||
if (shouldHoistLocalizedValue) {
|
||||
// replace actual value with localized value before sanitizing
|
||||
// { [locale]: fields } -> fields
|
||||
const value = siblingDoc[field.name][locale]
|
||||
|
||||
let hoistedValue = value
|
||||
|
||||
if (fallbackLocale && fallbackLocale !== locale) {
|
||||
const fallbackValue = siblingDoc[field.name][fallbackLocale]
|
||||
const isNullOrUndefined = typeof value === 'undefined' || value === null
|
||||
|
||||
if (fallbackValue) {
|
||||
switch (field.type) {
|
||||
case 'text':
|
||||
case 'textarea': {
|
||||
if (value === '' || isNullOrUndefined) {
|
||||
hoistedValue = fallbackValue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
if (isNullOrUndefined) {
|
||||
hoistedValue = fallbackValue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
siblingDoc[field.name] = hoistedValue
|
||||
}
|
||||
|
||||
// Sanitize outgoing field value
|
||||
switch (field.type) {
|
||||
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') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'point': {
|
||||
const pointDoc = siblingDoc[field.name] as Record<string, unknown>
|
||||
if (Array.isArray(pointDoc?.coordinates) && pointDoc.coordinates.length === 2) {
|
||||
siblingDoc[field.name] = pointDoc.coordinates
|
||||
} else {
|
||||
siblingDoc[field.name] = undefined
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
// Rich Text fields should use afterRead hooks to do population. The previous editor.populationPromises have been renamed to editor.graphQLPopulationPromises
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
field.tabs.forEach((tab) => {
|
||||
if (
|
||||
tabHasName(tab) &&
|
||||
(typeof siblingDoc[tab.name] === 'undefined' || siblingDoc[tab.name] === null)
|
||||
) {
|
||||
siblingDoc[tab.name] = {}
|
||||
}
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
// Execute hooks
|
||||
if (triggerHooks && field.hooks?.afterRead) {
|
||||
await field.hooks.afterRead.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const shouldRunHookOnAllLocales =
|
||||
field.localized &&
|
||||
(locale === 'all' || !flattenLocales) &&
|
||||
typeof siblingDoc[field.name] === 'object'
|
||||
|
||||
if (shouldRunHookOnAllLocales) {
|
||||
const hookPromises = Object.entries(siblingDoc[field.name]).map(([locale, value]) =>
|
||||
(async () => {
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
data: doc,
|
||||
depth,
|
||||
draft,
|
||||
field,
|
||||
findMany,
|
||||
global,
|
||||
operation: 'read',
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingData: siblingDoc,
|
||||
value,
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name][locale] = hookedValue
|
||||
}
|
||||
})(),
|
||||
)
|
||||
|
||||
await Promise.all(hookPromises)
|
||||
} else {
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
data: doc,
|
||||
depth,
|
||||
draft,
|
||||
field,
|
||||
findMany,
|
||||
global,
|
||||
operation: 'read',
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingData: siblingDoc,
|
||||
value: siblingDoc[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name] = hookedValue
|
||||
}
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
|
||||
// Execute access control
|
||||
let allowDefaultValue = true
|
||||
if (triggerAccessControl && field.access && field.access.read) {
|
||||
const result = overrideAccess
|
||||
? true
|
||||
: await field.access.read({
|
||||
id: doc.id as number | string,
|
||||
data: doc,
|
||||
doc,
|
||||
req,
|
||||
siblingData: siblingDoc,
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
allowDefaultValue = false
|
||||
delete siblingDoc[field.name]
|
||||
}
|
||||
}
|
||||
|
||||
// Set defaultValue on the field for globals being returned without being first created
|
||||
// or collection documents created prior to having a default
|
||||
if (
|
||||
allowDefaultValue &&
|
||||
typeof siblingDoc[field.name] === 'undefined' &&
|
||||
typeof field.defaultValue !== 'undefined'
|
||||
) {
|
||||
siblingDoc[field.name] = await getDefaultValue({
|
||||
defaultValue: field.defaultValue,
|
||||
locale,
|
||||
user: req.user,
|
||||
value: siblingDoc[field.name],
|
||||
})
|
||||
}
|
||||
|
||||
if (field.type === 'relationship' || field.type === 'upload' || field.type === 'join') {
|
||||
populationPromises.push(
|
||||
relationshipPopulationPromise({
|
||||
currentDepth,
|
||||
depth,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
field,
|
||||
locale,
|
||||
overrideAccess,
|
||||
populate,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
const rows = siblingDoc[field.name] as JsonObject
|
||||
|
||||
const arraySelect = select?.[field.name]
|
||||
|
||||
if (selectMode === 'include' && typeof arraySelect === 'object') {
|
||||
arraySelect.id = true
|
||||
}
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, i) => {
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: field.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select: typeof arraySelect === 'object' ? arraySelect : undefined,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
siblingDoc: row || {},
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
})
|
||||
} else if (!shouldHoistLocalizedValue && typeof rows === 'object' && rows !== null) {
|
||||
Object.values(rows).forEach((localeRows) => {
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row, i) => {
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: field.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingDoc: (row as JsonObject) || {},
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
siblingDoc[field.name] = []
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const rows = siblingDoc[field.name]
|
||||
|
||||
const blocksSelect = select?.[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, i) => {
|
||||
const block = field.blocks.find(
|
||||
(blockType) => blockType.slug === (row as JsonObject).blockType,
|
||||
)
|
||||
|
||||
let blockSelectMode = selectMode
|
||||
|
||||
if (typeof blocksSelect === 'object') {
|
||||
// sanitize blocks: {cta: false} to blocks: {cta: {id: true, blockType: true}}
|
||||
if (selectMode === 'exclude' && blocksSelect[block.slug] === false) {
|
||||
blockSelectMode = 'include'
|
||||
blocksSelect[block.slug] = {
|
||||
id: true,
|
||||
blockType: true,
|
||||
}
|
||||
} else if (selectMode === 'include') {
|
||||
if (!blocksSelect[block.slug]) {
|
||||
blocksSelect[block.slug] = {}
|
||||
}
|
||||
|
||||
if (typeof blocksSelect[block.slug] === 'object') {
|
||||
blocksSelect[block.slug]['id'] = true
|
||||
blocksSelect[block.slug]['blockType'] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const blockSelect = blocksSelect?.[block.slug]
|
||||
|
||||
if (block) {
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: block.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select: typeof blockSelect === 'object' ? blockSelect : undefined,
|
||||
selectMode: blockSelectMode,
|
||||
showHiddenFields,
|
||||
siblingDoc: (row as JsonObject) || {},
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (!shouldHoistLocalizedValue && typeof rows === 'object' && rows !== null) {
|
||||
Object.values(rows).forEach((localeRows) => {
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row, i) => {
|
||||
const block = field.blocks.find(
|
||||
(blockType) => blockType.slug === (row as JsonObject).blockType,
|
||||
)
|
||||
|
||||
if (block) {
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: block.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingDoc: (row as JsonObject) || {},
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
siblingDoc[field.name] = []
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'collapsible':
|
||||
|
||||
case 'row': {
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: field.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
case 'group': {
|
||||
let groupDoc = siblingDoc[field.name] as JsonObject
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
groupDoc = {}
|
||||
}
|
||||
|
||||
const groupSelect = select?.[field.name]
|
||||
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: field.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select: typeof groupSelect === 'object' ? groupSelect : undefined,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
siblingDoc: groupDoc,
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
if (editor?.hooks?.afterRead?.length) {
|
||||
await editor.hooks.afterRead.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const shouldRunHookOnAllLocales =
|
||||
field.localized &&
|
||||
(locale === 'all' || !flattenLocales) &&
|
||||
typeof siblingDoc[field.name] === 'object'
|
||||
|
||||
if (shouldRunHookOnAllLocales) {
|
||||
const hookPromises = Object.entries(siblingDoc[field.name]).map(([locale, value]) =>
|
||||
(async () => {
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
data: doc,
|
||||
depth,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
field,
|
||||
fieldPromises,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
operation: 'read',
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingData: siblingDoc,
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
value,
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name][locale] = hookedValue
|
||||
}
|
||||
})(),
|
||||
)
|
||||
|
||||
await Promise.all(hookPromises)
|
||||
} else {
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
data: doc,
|
||||
depth,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
field,
|
||||
fieldPromises,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
operation: 'read',
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingData: siblingDoc,
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
value: siblingDoc[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name] = hookedValue
|
||||
}
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'tab': {
|
||||
let tabDoc = siblingDoc
|
||||
let tabSelect: SelectType | undefined
|
||||
if (tabHasName(field)) {
|
||||
tabDoc = siblingDoc[field.name] as JsonObject
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
tabDoc = {}
|
||||
}
|
||||
|
||||
if (typeof select?.[field.name] === 'object') {
|
||||
tabSelect = select?.[field.name] as SelectType
|
||||
}
|
||||
} else {
|
||||
tabSelect = select
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: field.fields,
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select: tabSelect,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
siblingDoc: tabDoc,
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
fieldPromises,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,487 +0,0 @@
|
||||
import type { RichTextAdapter } from '../../../admin/RichText.js'
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { ValidationFieldError } from '../../../errors/index.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { RequestContext } from '../../../index.js'
|
||||
import type { JsonObject, Operation, PayloadRequest } from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js'
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { getExistingRowDoc } from './getExistingRowDoc.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args = {
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: JsonObject
|
||||
doc: JsonObject
|
||||
docWithLocales: JsonObject
|
||||
errors: ValidationFieldError[]
|
||||
field: Field | TabAsField
|
||||
/**
|
||||
* The index of the field as it appears in the parent's fields array. This is used to construct the field path / schemaPath
|
||||
* for unnamed fields like rows and collapsibles.
|
||||
*/
|
||||
fieldIndex: number
|
||||
global: null | SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
mergeLocaleActions: (() => Promise<void>)[]
|
||||
operation: Operation
|
||||
/**
|
||||
* The parent's path.
|
||||
*/
|
||||
parentPath: (number | string)[]
|
||||
/**
|
||||
* The parent's schemaPath (path without indexes).
|
||||
*/
|
||||
parentSchemaPath: string[]
|
||||
req: PayloadRequest
|
||||
siblingData: JsonObject
|
||||
siblingDoc: JsonObject
|
||||
siblingDocWithLocales?: JsonObject
|
||||
skipValidation: boolean
|
||||
}
|
||||
|
||||
// This function is responsible for the following actions, in order:
|
||||
// - Run condition
|
||||
// - Execute field hooks
|
||||
// - Validate data
|
||||
// - Transform data for storage
|
||||
// - beforeDuplicate hooks (if duplicate)
|
||||
// - Unflatten locales
|
||||
|
||||
export const promise = async ({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
field,
|
||||
fieldIndex,
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocWithLocales,
|
||||
skipValidation,
|
||||
}: Args): Promise<void> => {
|
||||
const passesCondition = field.admin?.condition
|
||||
? Boolean(field.admin.condition(data, siblingData, { user: req.user }))
|
||||
: true
|
||||
let skipValidationFromHere = skipValidation || !passesCondition
|
||||
const { localization } = req.payload.config
|
||||
const defaultLocale = localization ? localization?.defaultLocale : 'en'
|
||||
const operationLocale = req.locale || defaultLocale
|
||||
|
||||
const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data
|
||||
parentPath: parentPath.join('.'),
|
||||
parentSchemaPath: parentSchemaPath.join('.'),
|
||||
})
|
||||
const fieldPath = _fieldPath ? _fieldPath.split('.') : []
|
||||
const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : []
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
// skip validation if the field is localized and the incoming data is null
|
||||
if (field.localized && operationLocale !== defaultLocale) {
|
||||
if (['array', 'blocks'].includes(field.type) && siblingData[field.name] === null) {
|
||||
skipValidationFromHere = true
|
||||
}
|
||||
}
|
||||
|
||||
// Execute hooks
|
||||
if (field.hooks?.beforeChange) {
|
||||
await field.hooks.beforeChange.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
field,
|
||||
global,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
schemaPath: parentSchemaPath,
|
||||
siblingData,
|
||||
siblingDocWithLocales,
|
||||
value: siblingData[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingData[field.name] = hookedValue
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
|
||||
// Validate
|
||||
if (!skipValidationFromHere && 'validate' in field && field.validate) {
|
||||
const valueToValidate = siblingData[field.name]
|
||||
let jsonError: object
|
||||
|
||||
if (field.type === 'json' && typeof siblingData[field.name] === 'string') {
|
||||
try {
|
||||
JSON.parse(siblingData[field.name] as string)
|
||||
} catch (e) {
|
||||
jsonError = e
|
||||
}
|
||||
}
|
||||
|
||||
const validationResult = await field.validate(
|
||||
valueToValidate as never,
|
||||
{
|
||||
...field,
|
||||
id,
|
||||
collectionSlug: collection?.slug,
|
||||
data: deepMergeWithSourceArrays(doc, data),
|
||||
jsonError,
|
||||
operation,
|
||||
preferences: { fields: {} },
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
siblingData: deepMergeWithSourceArrays(siblingDoc, siblingData),
|
||||
} as any,
|
||||
)
|
||||
|
||||
if (typeof validationResult === 'string') {
|
||||
errors.push({
|
||||
message: validationResult,
|
||||
path: fieldPath.join('.'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Push merge locale action if applicable
|
||||
if (localization && field.localized) {
|
||||
mergeLocaleActions.push(async () => {
|
||||
const localeData = await localization.localeCodes.reduce(
|
||||
async (localizedValuesPromise: Promise<JsonObject>, locale) => {
|
||||
const localizedValues = await localizedValuesPromise
|
||||
const fieldValue =
|
||||
locale === req.locale
|
||||
? siblingData[field.name]
|
||||
: siblingDocWithLocales?.[field.name]?.[locale]
|
||||
|
||||
// const result = await localizedValues
|
||||
// update locale value if it's not undefined
|
||||
if (typeof fieldValue !== 'undefined') {
|
||||
return {
|
||||
...localizedValues,
|
||||
[locale]: fieldValue,
|
||||
}
|
||||
}
|
||||
|
||||
return localizedValuesPromise
|
||||
},
|
||||
Promise.resolve({}),
|
||||
)
|
||||
|
||||
// If there are locales with data, set the data
|
||||
if (Object.keys(localeData).length > 0) {
|
||||
siblingData[field.name] = localeData
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
const rows = siblingData[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
fields: field.fields,
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: row as JsonObject,
|
||||
siblingDoc: getExistingRowDoc(row as JsonObject, siblingDoc[field.name]),
|
||||
siblingDocWithLocales: getExistingRowDoc(
|
||||
row as JsonObject,
|
||||
siblingDocWithLocales[field.name],
|
||||
),
|
||||
skipValidation: skipValidationFromHere,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const rows = siblingData[field.name]
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
const rowSiblingDoc = getExistingRowDoc(row as JsonObject, siblingDoc[field.name])
|
||||
const rowSiblingDocWithLocales = getExistingRowDoc(
|
||||
row as JsonObject,
|
||||
siblingDocWithLocales ? siblingDocWithLocales[field.name] : {},
|
||||
)
|
||||
|
||||
const blockTypeToMatch = (row as JsonObject).blockType || rowSiblingDoc.blockType
|
||||
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
|
||||
|
||||
if (block) {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
fields: block.fields,
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: row as JsonObject,
|
||||
siblingDoc: rowSiblingDoc,
|
||||
siblingDocWithLocales: rowSiblingDocWithLocales,
|
||||
skipValidation: skipValidationFromHere,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'collapsible':
|
||||
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
fields: field.fields,
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocWithLocales,
|
||||
skipValidation: skipValidationFromHere,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'group': {
|
||||
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] = {}
|
||||
}
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
fields: field.fields,
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData[field.name] as JsonObject,
|
||||
siblingDoc: siblingDoc[field.name] as JsonObject,
|
||||
siblingDocWithLocales: siblingDocWithLocales[field.name] as JsonObject,
|
||||
skipValidation: skipValidationFromHere,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
case 'point': {
|
||||
// Transform point data for storage
|
||||
if (
|
||||
Array.isArray(siblingData[field.name]) &&
|
||||
siblingData[field.name][0] !== null &&
|
||||
siblingData[field.name][1] !== null
|
||||
) {
|
||||
siblingData[field.name] = {
|
||||
type: 'Point',
|
||||
coordinates: [
|
||||
parseFloat(siblingData[field.name][0]),
|
||||
parseFloat(siblingData[field.name][1]),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
if (editor?.hooks?.beforeChange?.length) {
|
||||
await editor.hooks.beforeChange.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
docWithLocales,
|
||||
errors,
|
||||
field,
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
schemaPath: parentSchemaPath,
|
||||
siblingData,
|
||||
siblingDocWithLocales,
|
||||
skipValidation,
|
||||
value: siblingData[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingData[field.name] = hookedValue
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tab': {
|
||||
let tabSiblingData = siblingData
|
||||
let tabSiblingDoc = siblingDoc
|
||||
let tabSiblingDocWithLocales = siblingDocWithLocales
|
||||
|
||||
if (tabHasName(field)) {
|
||||
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] = {}
|
||||
}
|
||||
|
||||
tabSiblingData = siblingData[field.name] as JsonObject
|
||||
tabSiblingDoc = siblingDoc[field.name] as JsonObject
|
||||
tabSiblingDocWithLocales = siblingDocWithLocales[field.name] as JsonObject
|
||||
}
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
fields: field.fields,
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: tabSiblingData,
|
||||
siblingDoc: tabSiblingDoc,
|
||||
siblingDocWithLocales: tabSiblingDocWithLocales,
|
||||
skipValidation: skipValidationFromHere,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocWithLocales,
|
||||
skipValidation: skipValidationFromHere,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RichTextAdapter } from '../../../admin/RichText.js'
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { ValidationFieldError } from '../../../errors/index.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
@@ -5,7 +6,11 @@ import type { RequestContext } from '../../../index.js'
|
||||
import type { JsonObject, Operation, PayloadRequest } from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { promise } from './promise.js'
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js'
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { getExistingRowDoc } from './getExistingRowDoc.js'
|
||||
|
||||
type Args = {
|
||||
collection: null | SanitizedCollectionConfig
|
||||
@@ -98,3 +103,475 @@ export const traverseFields = async ({
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
type PromiseArgs = {
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: JsonObject
|
||||
doc: JsonObject
|
||||
docWithLocales: JsonObject
|
||||
errors: ValidationFieldError[]
|
||||
field: Field | TabAsField
|
||||
/**
|
||||
* The index of the field as it appears in the parent's fields array. This is used to construct the field path / schemaPath
|
||||
* for unnamed fields like rows and collapsibles.
|
||||
*/
|
||||
fieldIndex: number
|
||||
global: null | SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
mergeLocaleActions: (() => Promise<void>)[]
|
||||
operation: Operation
|
||||
/**
|
||||
* The parent's path.
|
||||
*/
|
||||
parentPath: (number | string)[]
|
||||
/**
|
||||
* The parent's schemaPath (path without indexes).
|
||||
*/
|
||||
parentSchemaPath: string[]
|
||||
req: PayloadRequest
|
||||
siblingData: JsonObject
|
||||
siblingDoc: JsonObject
|
||||
siblingDocWithLocales?: JsonObject
|
||||
skipValidation: boolean
|
||||
}
|
||||
|
||||
// This function is responsible for the following actions, in order:
|
||||
// - Run condition
|
||||
// - Execute field hooks
|
||||
// - Validate data
|
||||
// - Transform data for storage
|
||||
// - beforeDuplicate hooks (if duplicate)
|
||||
// - Unflatten locales
|
||||
|
||||
export const promise = async ({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
field,
|
||||
fieldIndex,
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocWithLocales,
|
||||
skipValidation,
|
||||
}: PromiseArgs): Promise<void> => {
|
||||
const passesCondition = field.admin?.condition
|
||||
? Boolean(field.admin.condition(data, siblingData, { user: req.user }))
|
||||
: true
|
||||
let skipValidationFromHere = skipValidation || !passesCondition
|
||||
const { localization } = req.payload.config
|
||||
const defaultLocale = localization ? localization?.defaultLocale : 'en'
|
||||
const operationLocale = req.locale || defaultLocale
|
||||
|
||||
const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data
|
||||
parentPath: parentPath.join('.'),
|
||||
parentSchemaPath: parentSchemaPath.join('.'),
|
||||
})
|
||||
const fieldPath = _fieldPath ? _fieldPath.split('.') : []
|
||||
const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : []
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
// skip validation if the field is localized and the incoming data is null
|
||||
if (field.localized && operationLocale !== defaultLocale) {
|
||||
if (['array', 'blocks'].includes(field.type) && siblingData[field.name] === null) {
|
||||
skipValidationFromHere = true
|
||||
}
|
||||
}
|
||||
|
||||
// Execute hooks
|
||||
if (field.hooks?.beforeChange) {
|
||||
await field.hooks.beforeChange.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
field,
|
||||
global,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
schemaPath: parentSchemaPath,
|
||||
siblingData,
|
||||
siblingDocWithLocales,
|
||||
value: siblingData[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingData[field.name] = hookedValue
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
|
||||
// Validate
|
||||
if (!skipValidationFromHere && 'validate' in field && field.validate) {
|
||||
const valueToValidate = siblingData[field.name]
|
||||
let jsonError: object
|
||||
|
||||
if (field.type === 'json' && typeof siblingData[field.name] === 'string') {
|
||||
try {
|
||||
JSON.parse(siblingData[field.name] as string)
|
||||
} catch (e) {
|
||||
jsonError = e
|
||||
}
|
||||
}
|
||||
|
||||
const validationResult = await field.validate(
|
||||
valueToValidate as never,
|
||||
{
|
||||
...field,
|
||||
id,
|
||||
collectionSlug: collection?.slug,
|
||||
data: deepMergeWithSourceArrays(doc, data),
|
||||
jsonError,
|
||||
operation,
|
||||
preferences: { fields: {} },
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
siblingData: deepMergeWithSourceArrays(siblingDoc, siblingData),
|
||||
} as any,
|
||||
)
|
||||
|
||||
if (typeof validationResult === 'string') {
|
||||
errors.push({
|
||||
message: validationResult,
|
||||
path: fieldPath.join('.'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Push merge locale action if applicable
|
||||
if (localization && field.localized) {
|
||||
mergeLocaleActions.push(async () => {
|
||||
const localeData = await localization.localeCodes.reduce(
|
||||
async (localizedValuesPromise: Promise<JsonObject>, locale) => {
|
||||
const localizedValues = await localizedValuesPromise
|
||||
const fieldValue =
|
||||
locale === req.locale
|
||||
? siblingData[field.name]
|
||||
: siblingDocWithLocales?.[field.name]?.[locale]
|
||||
|
||||
// const result = await localizedValues
|
||||
// update locale value if it's not undefined
|
||||
if (typeof fieldValue !== 'undefined') {
|
||||
return {
|
||||
...localizedValues,
|
||||
[locale]: fieldValue,
|
||||
}
|
||||
}
|
||||
|
||||
return localizedValuesPromise
|
||||
},
|
||||
Promise.resolve({}),
|
||||
)
|
||||
|
||||
// If there are locales with data, set the data
|
||||
if (Object.keys(localeData).length > 0) {
|
||||
siblingData[field.name] = localeData
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
const rows = siblingData[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
fields: field.fields,
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: row as JsonObject,
|
||||
siblingDoc: getExistingRowDoc(row as JsonObject, siblingDoc[field.name]),
|
||||
siblingDocWithLocales: getExistingRowDoc(
|
||||
row as JsonObject,
|
||||
siblingDocWithLocales[field.name],
|
||||
),
|
||||
skipValidation: skipValidationFromHere,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const rows = siblingData[field.name]
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
const rowSiblingDoc = getExistingRowDoc(row as JsonObject, siblingDoc[field.name])
|
||||
const rowSiblingDocWithLocales = getExistingRowDoc(
|
||||
row as JsonObject,
|
||||
siblingDocWithLocales ? siblingDocWithLocales[field.name] : {},
|
||||
)
|
||||
|
||||
const blockTypeToMatch = (row as JsonObject).blockType || rowSiblingDoc.blockType
|
||||
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
|
||||
|
||||
if (block) {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
fields: block.fields,
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: row as JsonObject,
|
||||
siblingDoc: rowSiblingDoc,
|
||||
siblingDocWithLocales: rowSiblingDocWithLocales,
|
||||
skipValidation: skipValidationFromHere,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
fields: field.fields,
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocWithLocales,
|
||||
skipValidation: skipValidationFromHere,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'group': {
|
||||
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] = {}
|
||||
}
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
fields: field.fields,
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData[field.name] as JsonObject,
|
||||
siblingDoc: siblingDoc[field.name] as JsonObject,
|
||||
siblingDocWithLocales: siblingDocWithLocales[field.name] as JsonObject,
|
||||
skipValidation: skipValidationFromHere,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
case 'point': {
|
||||
// Transform point data for storage
|
||||
if (
|
||||
Array.isArray(siblingData[field.name]) &&
|
||||
siblingData[field.name][0] !== null &&
|
||||
siblingData[field.name][1] !== null
|
||||
) {
|
||||
siblingData[field.name] = {
|
||||
type: 'Point',
|
||||
coordinates: [
|
||||
parseFloat(siblingData[field.name][0]),
|
||||
parseFloat(siblingData[field.name][1]),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
if (editor?.hooks?.beforeChange?.length) {
|
||||
await editor.hooks.beforeChange.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
docWithLocales,
|
||||
errors,
|
||||
field,
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
schemaPath: parentSchemaPath,
|
||||
siblingData,
|
||||
siblingDocWithLocales,
|
||||
skipValidation,
|
||||
value: siblingData[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingData[field.name] = hookedValue
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tab': {
|
||||
let tabSiblingData = siblingData
|
||||
let tabSiblingDoc = siblingDoc
|
||||
let tabSiblingDocWithLocales = siblingDocWithLocales
|
||||
|
||||
if (tabHasName(field)) {
|
||||
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] = {}
|
||||
}
|
||||
|
||||
tabSiblingData = siblingData[field.name] as JsonObject
|
||||
tabSiblingDoc = siblingDoc[field.name] as JsonObject
|
||||
tabSiblingDocWithLocales = siblingDocWithLocales[field.name] as JsonObject
|
||||
}
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
fields: field.fields,
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: tabSiblingData,
|
||||
siblingDoc: tabSiblingDoc,
|
||||
siblingDocWithLocales: tabSiblingDocWithLocales,
|
||||
skipValidation: skipValidationFromHere,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocWithLocales,
|
||||
skipValidation: skipValidationFromHere,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { RequestContext } from '../../../index.js'
|
||||
import type { JsonObject, PayloadRequest } from '../../../types/index.js'
|
||||
import type { Field, FieldHookArgs, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { runBeforeDuplicateHooks } from './runHook.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args<T> = {
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
doc: T
|
||||
field: Field | TabAsField
|
||||
fieldIndex: number
|
||||
id?: number | string
|
||||
overrideAccess: boolean
|
||||
parentPath: (number | string)[]
|
||||
parentSchemaPath: string[]
|
||||
req: PayloadRequest
|
||||
siblingDoc: JsonObject
|
||||
}
|
||||
|
||||
export const promise = async <T>({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
field,
|
||||
fieldIndex,
|
||||
overrideAccess,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
req,
|
||||
siblingDoc,
|
||||
}: Args<T>): Promise<void> => {
|
||||
const { localization } = req.payload.config
|
||||
|
||||
const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data
|
||||
parentPath: parentPath.join('.'),
|
||||
parentSchemaPath: parentSchemaPath.join('.'),
|
||||
})
|
||||
const fieldPath = _fieldPath ? _fieldPath.split('.') : []
|
||||
const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : []
|
||||
|
||||
// Handle unnamed tabs
|
||||
if (field.type === 'tab' && !tabHasName(field)) {
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
let fieldData = siblingDoc?.[field.name]
|
||||
const fieldIsLocalized = field.localized && localization
|
||||
|
||||
// Run field beforeDuplicate hooks
|
||||
if (Array.isArray(field.hooks?.beforeDuplicate)) {
|
||||
if (fieldIsLocalized) {
|
||||
const localeData = await localization.localeCodes.reduce(
|
||||
async (localizedValuesPromise: Promise<JsonObject>, locale) => {
|
||||
const localizedValues = await localizedValuesPromise
|
||||
|
||||
const beforeDuplicateArgs: FieldHookArgs = {
|
||||
collection,
|
||||
context,
|
||||
data: doc,
|
||||
field,
|
||||
global: undefined,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name]?.[locale],
|
||||
req,
|
||||
schemaPath: parentSchemaPath,
|
||||
siblingData: siblingDoc,
|
||||
siblingDocWithLocales: siblingDoc,
|
||||
value: siblingDoc[field.name]?.[locale],
|
||||
}
|
||||
|
||||
const hookResult = await runBeforeDuplicateHooks(beforeDuplicateArgs)
|
||||
|
||||
if (typeof hookResult !== 'undefined') {
|
||||
return {
|
||||
...localizedValues,
|
||||
[locale]: hookResult,
|
||||
}
|
||||
}
|
||||
|
||||
return localizedValuesPromise
|
||||
},
|
||||
Promise.resolve({}),
|
||||
)
|
||||
|
||||
siblingDoc[field.name] = localeData
|
||||
} else {
|
||||
const beforeDuplicateArgs: FieldHookArgs = {
|
||||
collection,
|
||||
context,
|
||||
data: doc,
|
||||
field,
|
||||
global: undefined,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
schemaPath: parentSchemaPath,
|
||||
siblingData: siblingDoc,
|
||||
siblingDocWithLocales: siblingDoc,
|
||||
value: siblingDoc[field.name],
|
||||
}
|
||||
|
||||
const hookResult = await runBeforeDuplicateHooks(beforeDuplicateArgs)
|
||||
if (typeof hookResult !== 'undefined') {
|
||||
siblingDoc[field.name] = hookResult
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// First, for any localized fields, we will loop over locales
|
||||
// and if locale data is present, traverse the sub fields.
|
||||
// There are only a few different fields where this is possible.
|
||||
if (fieldIsLocalized) {
|
||||
if (typeof fieldData !== 'object' || fieldData === null) {
|
||||
siblingDoc[field.name] = {}
|
||||
fieldData = siblingDoc[field.name]
|
||||
}
|
||||
|
||||
const promises = []
|
||||
|
||||
localization.localeCodes.forEach((locale) => {
|
||||
if (fieldData[locale]) {
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
const rows = fieldData[locale]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: row,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'blocks': {
|
||||
const rows = fieldData[locale]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
const blockTypeToMatch = row.blockType
|
||||
|
||||
const block = field.blocks.find(
|
||||
(blockType) => blockType.slug === blockTypeToMatch,
|
||||
)
|
||||
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: block.fields,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: row,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'group':
|
||||
|
||||
case 'tab': {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
path: fieldSchemaPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: fieldData[locale],
|
||||
}),
|
||||
)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
} else {
|
||||
// If the field is not localized, but it affects data,
|
||||
// we need to further traverse its children
|
||||
// so the child fields can run beforeDuplicate hooks
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
const rows = siblingDoc[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: row,
|
||||
}),
|
||||
)
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'blocks': {
|
||||
const rows = siblingDoc[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
const blockTypeToMatch = row.blockType
|
||||
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
|
||||
|
||||
if (block) {
|
||||
;(row as JsonObject).blockType = blockTypeToMatch
|
||||
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: block.fields,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: row,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'group':
|
||||
|
||||
case 'tab': {
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
|
||||
const groupDoc = siblingDoc[field.name] as Record<string, unknown>
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: groupDoc as JsonObject,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Finally, we traverse fields which do not affect data here
|
||||
switch (field.type) {
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { RequestContext } from '../../../index.js'
|
||||
import type { JsonObject, PayloadRequest } from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
import type { Field, FieldHookArgs, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { promise } from './promise.js'
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { runBeforeDuplicateHooks } from './runHook.js'
|
||||
|
||||
type Args<T> = {
|
||||
collection: null | SanitizedCollectionConfig
|
||||
@@ -50,3 +52,355 @@ export const traverseFields = async <T>({
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
type PromiseArgs<T> = {
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
doc: T
|
||||
field: Field | TabAsField
|
||||
fieldIndex: number
|
||||
id?: number | string
|
||||
overrideAccess: boolean
|
||||
parentPath: (number | string)[]
|
||||
parentSchemaPath: string[]
|
||||
req: PayloadRequest
|
||||
siblingDoc: JsonObject
|
||||
}
|
||||
|
||||
export const promise = async <T>({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
field,
|
||||
fieldIndex,
|
||||
overrideAccess,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
req,
|
||||
siblingDoc,
|
||||
}: PromiseArgs<T>): Promise<void> => {
|
||||
const { localization } = req.payload.config
|
||||
|
||||
const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data
|
||||
parentPath: parentPath.join('.'),
|
||||
parentSchemaPath: parentSchemaPath.join('.'),
|
||||
})
|
||||
const fieldPath = _fieldPath ? _fieldPath.split('.') : []
|
||||
const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : []
|
||||
|
||||
// Handle unnamed tabs
|
||||
if (field.type === 'tab' && !tabHasName(field)) {
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
let fieldData = siblingDoc?.[field.name]
|
||||
const fieldIsLocalized = field.localized && localization
|
||||
|
||||
// Run field beforeDuplicate hooks
|
||||
if (Array.isArray(field.hooks?.beforeDuplicate)) {
|
||||
if (fieldIsLocalized) {
|
||||
const localeData = await localization.localeCodes.reduce(
|
||||
async (localizedValuesPromise: Promise<JsonObject>, locale) => {
|
||||
const localizedValues = await localizedValuesPromise
|
||||
|
||||
const beforeDuplicateArgs: FieldHookArgs = {
|
||||
collection,
|
||||
context,
|
||||
data: doc,
|
||||
field,
|
||||
global: undefined,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name]?.[locale],
|
||||
req,
|
||||
schemaPath: parentSchemaPath,
|
||||
siblingData: siblingDoc,
|
||||
siblingDocWithLocales: siblingDoc,
|
||||
value: siblingDoc[field.name]?.[locale],
|
||||
}
|
||||
|
||||
const hookResult = await runBeforeDuplicateHooks(beforeDuplicateArgs)
|
||||
|
||||
if (typeof hookResult !== 'undefined') {
|
||||
return {
|
||||
...localizedValues,
|
||||
[locale]: hookResult,
|
||||
}
|
||||
}
|
||||
|
||||
return localizedValuesPromise
|
||||
},
|
||||
Promise.resolve({}),
|
||||
)
|
||||
|
||||
siblingDoc[field.name] = localeData
|
||||
} else {
|
||||
const beforeDuplicateArgs: FieldHookArgs = {
|
||||
collection,
|
||||
context,
|
||||
data: doc,
|
||||
field,
|
||||
global: undefined,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
schemaPath: parentSchemaPath,
|
||||
siblingData: siblingDoc,
|
||||
siblingDocWithLocales: siblingDoc,
|
||||
value: siblingDoc[field.name],
|
||||
}
|
||||
|
||||
const hookResult = await runBeforeDuplicateHooks(beforeDuplicateArgs)
|
||||
if (typeof hookResult !== 'undefined') {
|
||||
siblingDoc[field.name] = hookResult
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// First, for any localized fields, we will loop over locales
|
||||
// and if locale data is present, traverse the sub fields.
|
||||
// There are only a few different fields where this is possible.
|
||||
if (fieldIsLocalized) {
|
||||
if (typeof fieldData !== 'object' || fieldData === null) {
|
||||
siblingDoc[field.name] = {}
|
||||
fieldData = siblingDoc[field.name]
|
||||
}
|
||||
|
||||
const promises = []
|
||||
|
||||
localization.localeCodes.forEach((locale) => {
|
||||
if (fieldData[locale]) {
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
const rows = fieldData[locale]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: row,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'blocks': {
|
||||
const rows = fieldData[locale]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
const blockTypeToMatch = row.blockType
|
||||
|
||||
const block = field.blocks.find(
|
||||
(blockType) => blockType.slug === blockTypeToMatch,
|
||||
)
|
||||
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: block.fields,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: row,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'group':
|
||||
|
||||
case 'tab': {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
path: fieldSchemaPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: fieldData[locale],
|
||||
}),
|
||||
)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
} else {
|
||||
// If the field is not localized, but it affects data,
|
||||
// we need to further traverse its children
|
||||
// so the child fields can run beforeDuplicate hooks
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
const rows = siblingDoc[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: row,
|
||||
}),
|
||||
)
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'blocks': {
|
||||
const rows = siblingDoc[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
const blockTypeToMatch = row.blockType
|
||||
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
|
||||
|
||||
if (block) {
|
||||
;(row as JsonObject).blockType = blockTypeToMatch
|
||||
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: block.fields,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: row,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'group':
|
||||
|
||||
case 'tab': {
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
|
||||
const groupDoc = siblingDoc[field.name] as Record<string, unknown>
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: groupDoc as JsonObject,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Finally, we traverse fields which do not affect data here
|
||||
switch (field.type) {
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,552 +0,0 @@
|
||||
import type { RichTextAdapter } from '../../../admin/RichText.js'
|
||||
import type { SanitizedCollectionConfig, TypeWithID } from '../../../collections/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { RequestContext } from '../../../index.js'
|
||||
import type { JsonObject, JsonValue, PayloadRequest } from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { fieldAffectsData, tabHasName, valueIsValueWithRelation } from '../../config/types.js'
|
||||
import { getDefaultValue } from '../../getDefaultValue.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { cloneDataFromOriginalDoc } from '../beforeChange/cloneDataFromOriginalDoc.js'
|
||||
import { getExistingRowDoc } from '../beforeChange/getExistingRowDoc.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args<T> = {
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: T
|
||||
/**
|
||||
* The original data (not modified by any hooks)
|
||||
*/
|
||||
doc: T
|
||||
field: Field | TabAsField
|
||||
fieldIndex: number
|
||||
global: null | SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
operation: 'create' | 'update'
|
||||
overrideAccess: boolean
|
||||
parentPath: (number | string)[]
|
||||
parentSchemaPath: string[]
|
||||
req: PayloadRequest
|
||||
siblingData: JsonObject
|
||||
/**
|
||||
* The original siblingData (not modified by any hooks)
|
||||
*/
|
||||
siblingDoc: JsonObject
|
||||
}
|
||||
|
||||
// This function is responsible for the following actions, in order:
|
||||
// - Sanitize incoming data
|
||||
// - Execute field hooks
|
||||
// - Execute field access control
|
||||
// - Merge original document data into incoming data
|
||||
// - Compute default values for undefined fields
|
||||
|
||||
export const promise = async <T>({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
field,
|
||||
fieldIndex,
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}: Args<T>): Promise<void> => {
|
||||
const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data
|
||||
parentPath: parentPath.join('.'),
|
||||
parentSchemaPath: parentSchemaPath.join('.'),
|
||||
})
|
||||
const fieldPath = _fieldPath ? _fieldPath.split('.') : []
|
||||
const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : []
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
if (field.name === 'id') {
|
||||
if (field.type === 'number' && typeof siblingData[field.name] === 'string') {
|
||||
const value = siblingData[field.name] as string
|
||||
|
||||
siblingData[field.name] = parseFloat(value)
|
||||
}
|
||||
|
||||
if (
|
||||
field.type === 'text' &&
|
||||
typeof siblingData[field.name]?.toString === 'function' &&
|
||||
typeof siblingData[field.name] !== 'string'
|
||||
) {
|
||||
siblingData[field.name] = siblingData[field.name].toString()
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize incoming data
|
||||
switch (field.type) {
|
||||
case 'array':
|
||||
case 'blocks': {
|
||||
// Handle cases of arrays being intentionally set to 0
|
||||
if (siblingData[field.name] === '0' || siblingData[field.name] === 0) {
|
||||
siblingData[field.name] = []
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'checkbox': {
|
||||
if (siblingData[field.name] === 'true') {
|
||||
siblingData[field.name] = true
|
||||
}
|
||||
if (siblingData[field.name] === 'false') {
|
||||
siblingData[field.name] = false
|
||||
}
|
||||
if (siblingData[field.name] === '') {
|
||||
siblingData[field.name] = false
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'number': {
|
||||
if (typeof siblingData[field.name] === 'string') {
|
||||
const value = siblingData[field.name] as string
|
||||
const trimmed = value.trim()
|
||||
siblingData[field.name] = trimmed.length === 0 ? null : parseFloat(trimmed)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'point': {
|
||||
if (Array.isArray(siblingData[field.name])) {
|
||||
siblingData[field.name] = (siblingData[field.name] as string[]).map((coordinate, i) => {
|
||||
if (typeof coordinate === 'string') {
|
||||
const value = siblingData[field.name][i] as string
|
||||
const trimmed = value.trim()
|
||||
return trimmed.length === 0 ? null : parseFloat(trimmed)
|
||||
}
|
||||
return coordinate
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'relationship':
|
||||
case 'upload': {
|
||||
if (
|
||||
siblingData[field.name] === '' ||
|
||||
siblingData[field.name] === 'none' ||
|
||||
siblingData[field.name] === 'null' ||
|
||||
siblingData[field.name] === null
|
||||
) {
|
||||
if (field.hasMany === true) {
|
||||
siblingData[field.name] = []
|
||||
} else {
|
||||
siblingData[field.name] = null
|
||||
}
|
||||
}
|
||||
|
||||
const value = siblingData[field.name]
|
||||
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((relatedDoc: { relationTo: string; value: JsonValue }, i) => {
|
||||
const relatedCollection = req.payload.config.collections.find(
|
||||
(collection) => collection.slug === relatedDoc.relationTo,
|
||||
)
|
||||
|
||||
if (
|
||||
typeof relatedDoc.value === 'object' &&
|
||||
relatedDoc.value &&
|
||||
'id' in relatedDoc.value
|
||||
) {
|
||||
relatedDoc.value = relatedDoc.value.id
|
||||
}
|
||||
|
||||
if (relatedCollection?.fields) {
|
||||
const relationshipIDField = relatedCollection.fields.find(
|
||||
(collectionField) =>
|
||||
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
||||
)
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name][i] = {
|
||||
...relatedDoc,
|
||||
value: parseFloat(relatedDoc.value as string),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
if (field.hasMany !== true && valueIsValueWithRelation(value)) {
|
||||
const relatedCollection = req.payload.config.collections.find(
|
||||
(collection) => collection.slug === value.relationTo,
|
||||
)
|
||||
|
||||
if (typeof value.value === 'object' && value.value && 'id' in value.value) {
|
||||
value.value = (value.value as TypeWithID).id
|
||||
}
|
||||
|
||||
if (relatedCollection?.fields) {
|
||||
const relationshipIDField = relatedCollection.fields.find(
|
||||
(collectionField) =>
|
||||
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
||||
)
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name] = { ...value, value: parseFloat(value.value as string) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((relatedDoc: unknown, i) => {
|
||||
const relatedCollection = req.payload.config.collections.find(
|
||||
(collection) => collection.slug === field.relationTo,
|
||||
)
|
||||
|
||||
if (typeof relatedDoc === 'object' && relatedDoc && 'id' in relatedDoc) {
|
||||
value[i] = relatedDoc.id
|
||||
}
|
||||
|
||||
if (relatedCollection?.fields) {
|
||||
const relationshipIDField = relatedCollection.fields.find(
|
||||
(collectionField) =>
|
||||
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
||||
)
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name][i] = parseFloat(relatedDoc as string)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
if (field.hasMany !== true && value) {
|
||||
const relatedCollection = req.payload.config.collections.find(
|
||||
(collection) => collection.slug === field.relationTo,
|
||||
)
|
||||
|
||||
if (typeof value === 'object' && value && 'id' in value) {
|
||||
siblingData[field.name] = value.id
|
||||
}
|
||||
|
||||
if (relatedCollection?.fields) {
|
||||
const relationshipIDField = relatedCollection.fields.find(
|
||||
(collectionField) =>
|
||||
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
||||
)
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name] = parseFloat(value as string)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'richText': {
|
||||
if (typeof siblingData[field.name] === 'string') {
|
||||
try {
|
||||
const richTextJSON = JSON.parse(siblingData[field.name] as string)
|
||||
siblingData[field.name] = richTextJSON
|
||||
} catch {
|
||||
// Disregard this data as it is not valid.
|
||||
// Will be reported to user by field validation
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Execute hooks
|
||||
if (field.hooks?.beforeValidate) {
|
||||
await field.hooks.beforeValidate.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
field,
|
||||
global,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingData[field.name],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
value: siblingData[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingData[field.name] = hookedValue
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
|
||||
// Execute access control
|
||||
if (field.access && field.access[operation]) {
|
||||
const result = overrideAccess
|
||||
? true
|
||||
: await field.access[operation]({ id, data, doc, req, siblingData })
|
||||
|
||||
if (!result) {
|
||||
delete siblingData[field.name]
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof siblingData[field.name] === 'undefined') {
|
||||
// If no incoming data, but existing document data is found, merge it in
|
||||
if (typeof siblingDoc[field.name] !== 'undefined') {
|
||||
siblingData[field.name] = cloneDataFromOriginalDoc(siblingDoc[field.name])
|
||||
|
||||
// Otherwise compute default value
|
||||
} else if (typeof field.defaultValue !== 'undefined') {
|
||||
siblingData[field.name] = await getDefaultValue({
|
||||
defaultValue: field.defaultValue,
|
||||
locale: req.locale,
|
||||
user: req.user,
|
||||
value: siblingData[field.name],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse subfields
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
const rows = siblingData[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: row as JsonObject,
|
||||
siblingDoc: getExistingRowDoc(row as JsonObject, siblingDoc[field.name]),
|
||||
}),
|
||||
)
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const rows = siblingData[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
const rowSiblingDoc = getExistingRowDoc(row as JsonObject, siblingDoc[field.name])
|
||||
const blockTypeToMatch = (row as JsonObject).blockType || rowSiblingDoc.blockType
|
||||
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
|
||||
|
||||
if (block) {
|
||||
;(row as JsonObject).blockType = blockTypeToMatch
|
||||
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: block.fields,
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: row as JsonObject,
|
||||
siblingDoc: rowSiblingDoc,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
case 'group': {
|
||||
if (typeof siblingData[field.name] !== 'object') {
|
||||
siblingData[field.name] = {}
|
||||
}
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
|
||||
const groupData = siblingData[field.name] as Record<string, unknown>
|
||||
const groupDoc = siblingDoc[field.name] as Record<string, unknown>
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: groupData as JsonObject,
|
||||
siblingDoc: groupDoc as JsonObject,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
if (editor?.hooks?.beforeValidate?.length) {
|
||||
await editor.hooks.beforeValidate.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
field,
|
||||
global,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingData[field.name],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
value: siblingData[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingData[field.name] = hookedValue
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'tab': {
|
||||
let tabSiblingData
|
||||
let tabSiblingDoc
|
||||
if (tabHasName(field)) {
|
||||
if (typeof siblingData[field.name] !== 'object') {
|
||||
siblingData[field.name] = {}
|
||||
}
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
|
||||
tabSiblingData = siblingData[field.name] as Record<string, unknown>
|
||||
tabSiblingDoc = siblingDoc[field.name] as Record<string, unknown>
|
||||
} else {
|
||||
tabSiblingData = siblingData
|
||||
tabSiblingDoc = siblingDoc
|
||||
}
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: tabSiblingData,
|
||||
siblingDoc: tabSiblingDoc,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
|
||||
import type { RichTextAdapter } from '../../../admin/RichText.js'
|
||||
import type { SanitizedCollectionConfig, TypeWithID } from '../../../collections/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { RequestContext } from '../../../index.js'
|
||||
import type { JsonObject, PayloadRequest } from '../../../types/index.js'
|
||||
import type { JsonObject, JsonValue, PayloadRequest } from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { promise } from './promise.js'
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { fieldAffectsData, tabHasName, valueIsValueWithRelation } from '../../config/types.js'
|
||||
import { getDefaultValue } from '../../getDefaultValue.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { cloneDataFromOriginalDoc } from '../beforeChange/cloneDataFromOriginalDoc.js'
|
||||
import { getExistingRowDoc } from '../beforeChange/getExistingRowDoc.js'
|
||||
|
||||
type Args<T> = {
|
||||
collection: null | SanitizedCollectionConfig
|
||||
@@ -69,3 +75,541 @@ export const traverseFields = async <T>({
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
type PromiseArgs<T> = {
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: T
|
||||
/**
|
||||
* The original data (not modified by any hooks)
|
||||
*/
|
||||
doc: T
|
||||
field: Field | TabAsField
|
||||
fieldIndex: number
|
||||
global: null | SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
operation: 'create' | 'update'
|
||||
overrideAccess: boolean
|
||||
parentPath: (number | string)[]
|
||||
parentSchemaPath: string[]
|
||||
req: PayloadRequest
|
||||
siblingData: JsonObject
|
||||
/**
|
||||
* The original siblingData (not modified by any hooks)
|
||||
*/
|
||||
siblingDoc: JsonObject
|
||||
}
|
||||
|
||||
// This function is responsible for the following actions, in order:
|
||||
// - Sanitize incoming data
|
||||
// - Execute field hooks
|
||||
// - Execute field access control
|
||||
// - Merge original document data into incoming data
|
||||
// - Compute default values for undefined fields
|
||||
|
||||
export const promise = async <T>({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
field,
|
||||
fieldIndex,
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}: PromiseArgs<T>): Promise<void> => {
|
||||
const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data
|
||||
parentPath: parentPath.join('.'),
|
||||
parentSchemaPath: parentSchemaPath.join('.'),
|
||||
})
|
||||
const fieldPath = _fieldPath ? _fieldPath.split('.') : []
|
||||
const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : []
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
if (field.name === 'id') {
|
||||
if (field.type === 'number' && typeof siblingData[field.name] === 'string') {
|
||||
const value = siblingData[field.name] as string
|
||||
|
||||
siblingData[field.name] = parseFloat(value)
|
||||
}
|
||||
|
||||
if (
|
||||
field.type === 'text' &&
|
||||
typeof siblingData[field.name]?.toString === 'function' &&
|
||||
typeof siblingData[field.name] !== 'string'
|
||||
) {
|
||||
siblingData[field.name] = siblingData[field.name].toString()
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize incoming data
|
||||
switch (field.type) {
|
||||
case 'array':
|
||||
case 'blocks': {
|
||||
// Handle cases of arrays being intentionally set to 0
|
||||
if (siblingData[field.name] === '0' || siblingData[field.name] === 0) {
|
||||
siblingData[field.name] = []
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'checkbox': {
|
||||
if (siblingData[field.name] === 'true') {
|
||||
siblingData[field.name] = true
|
||||
}
|
||||
if (siblingData[field.name] === 'false') {
|
||||
siblingData[field.name] = false
|
||||
}
|
||||
if (siblingData[field.name] === '') {
|
||||
siblingData[field.name] = false
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'number': {
|
||||
if (typeof siblingData[field.name] === 'string') {
|
||||
const value = siblingData[field.name] as string
|
||||
const trimmed = value.trim()
|
||||
siblingData[field.name] = trimmed.length === 0 ? null : parseFloat(trimmed)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'point': {
|
||||
if (Array.isArray(siblingData[field.name])) {
|
||||
siblingData[field.name] = (siblingData[field.name] as string[]).map((coordinate, i) => {
|
||||
if (typeof coordinate === 'string') {
|
||||
const value = siblingData[field.name][i] as string
|
||||
const trimmed = value.trim()
|
||||
return trimmed.length === 0 ? null : parseFloat(trimmed)
|
||||
}
|
||||
return coordinate
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'relationship':
|
||||
case 'upload': {
|
||||
if (
|
||||
siblingData[field.name] === '' ||
|
||||
siblingData[field.name] === 'none' ||
|
||||
siblingData[field.name] === 'null' ||
|
||||
siblingData[field.name] === null
|
||||
) {
|
||||
if (field.hasMany === true) {
|
||||
siblingData[field.name] = []
|
||||
} else {
|
||||
siblingData[field.name] = null
|
||||
}
|
||||
}
|
||||
|
||||
const value = siblingData[field.name]
|
||||
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((relatedDoc: { relationTo: string; value: JsonValue }, i) => {
|
||||
const relatedCollection = req.payload.config.collections.find(
|
||||
(collection) => collection.slug === relatedDoc.relationTo,
|
||||
)
|
||||
|
||||
if (
|
||||
typeof relatedDoc.value === 'object' &&
|
||||
relatedDoc.value &&
|
||||
'id' in relatedDoc.value
|
||||
) {
|
||||
relatedDoc.value = relatedDoc.value.id
|
||||
}
|
||||
|
||||
if (relatedCollection?.fields) {
|
||||
const relationshipIDField = relatedCollection.fields.find(
|
||||
(collectionField) =>
|
||||
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
||||
)
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name][i] = {
|
||||
...relatedDoc,
|
||||
value: parseFloat(relatedDoc.value as string),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
if (field.hasMany !== true && valueIsValueWithRelation(value)) {
|
||||
const relatedCollection = req.payload.config.collections.find(
|
||||
(collection) => collection.slug === value.relationTo,
|
||||
)
|
||||
|
||||
if (typeof value.value === 'object' && value.value && 'id' in value.value) {
|
||||
value.value = (value.value as TypeWithID).id
|
||||
}
|
||||
|
||||
if (relatedCollection?.fields) {
|
||||
const relationshipIDField = relatedCollection.fields.find(
|
||||
(collectionField) =>
|
||||
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
||||
)
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name] = { ...value, value: parseFloat(value.value as string) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((relatedDoc: unknown, i) => {
|
||||
const relatedCollection = req.payload.config.collections.find(
|
||||
(collection) => collection.slug === field.relationTo,
|
||||
)
|
||||
|
||||
if (typeof relatedDoc === 'object' && relatedDoc && 'id' in relatedDoc) {
|
||||
value[i] = relatedDoc.id
|
||||
}
|
||||
|
||||
if (relatedCollection?.fields) {
|
||||
const relationshipIDField = relatedCollection.fields.find(
|
||||
(collectionField) =>
|
||||
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
||||
)
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name][i] = parseFloat(relatedDoc as string)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
if (field.hasMany !== true && value) {
|
||||
const relatedCollection = req.payload.config.collections.find(
|
||||
(collection) => collection.slug === field.relationTo,
|
||||
)
|
||||
|
||||
if (typeof value === 'object' && value && 'id' in value) {
|
||||
siblingData[field.name] = value.id
|
||||
}
|
||||
|
||||
if (relatedCollection?.fields) {
|
||||
const relationshipIDField = relatedCollection.fields.find(
|
||||
(collectionField) =>
|
||||
fieldAffectsData(collectionField) && collectionField.name === 'id',
|
||||
)
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name] = parseFloat(value as string)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'richText': {
|
||||
if (typeof siblingData[field.name] === 'string') {
|
||||
try {
|
||||
const richTextJSON = JSON.parse(siblingData[field.name] as string)
|
||||
siblingData[field.name] = richTextJSON
|
||||
} catch {
|
||||
// Disregard this data as it is not valid.
|
||||
// Will be reported to user by field validation
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Execute hooks
|
||||
if (field.hooks?.beforeValidate) {
|
||||
await field.hooks.beforeValidate.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
field,
|
||||
global,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingData[field.name],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
value: siblingData[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingData[field.name] = hookedValue
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
|
||||
// Execute access control
|
||||
if (field.access && field.access[operation]) {
|
||||
const result = overrideAccess
|
||||
? true
|
||||
: await field.access[operation]({ id, data, doc, req, siblingData })
|
||||
|
||||
if (!result) {
|
||||
delete siblingData[field.name]
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof siblingData[field.name] === 'undefined') {
|
||||
// If no incoming data, but existing document data is found, merge it in
|
||||
if (typeof siblingDoc[field.name] !== 'undefined') {
|
||||
siblingData[field.name] = cloneDataFromOriginalDoc(siblingDoc[field.name])
|
||||
|
||||
// Otherwise compute default value
|
||||
} else if (typeof field.defaultValue !== 'undefined') {
|
||||
siblingData[field.name] = await getDefaultValue({
|
||||
defaultValue: field.defaultValue,
|
||||
locale: req.locale,
|
||||
user: req.user,
|
||||
value: siblingData[field.name],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse subfields
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
const rows = siblingData[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: row as JsonObject,
|
||||
siblingDoc: getExistingRowDoc(row as JsonObject, siblingDoc[field.name]),
|
||||
}),
|
||||
)
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const rows = siblingData[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, i) => {
|
||||
const rowSiblingDoc = getExistingRowDoc(row as JsonObject, siblingDoc[field.name])
|
||||
const blockTypeToMatch = (row as JsonObject).blockType || rowSiblingDoc.blockType
|
||||
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
|
||||
|
||||
if (block) {
|
||||
;(row as JsonObject).blockType = blockTypeToMatch
|
||||
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: block.fields,
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: row as JsonObject,
|
||||
siblingDoc: rowSiblingDoc,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
case 'group': {
|
||||
if (typeof siblingData[field.name] !== 'object') {
|
||||
siblingData[field.name] = {}
|
||||
}
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
|
||||
const groupData = siblingData[field.name] as Record<string, unknown>
|
||||
const groupDoc = siblingDoc[field.name] as Record<string, unknown>
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: groupData as JsonObject,
|
||||
siblingDoc: groupDoc as JsonObject,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
if (editor?.hooks?.beforeValidate?.length) {
|
||||
await editor.hooks.beforeValidate.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
field,
|
||||
global,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingData[field.name],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
value: siblingData[field.name],
|
||||
})
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingData[field.name] = hookedValue
|
||||
}
|
||||
}, Promise.resolve())
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'tab': {
|
||||
let tabSiblingData
|
||||
let tabSiblingDoc
|
||||
if (tabHasName(field)) {
|
||||
if (typeof siblingData[field.name] !== 'object') {
|
||||
siblingData[field.name] = {}
|
||||
}
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
|
||||
tabSiblingData = siblingData[field.name] as Record<string, unknown>
|
||||
tabSiblingDoc = siblingDoc[field.name] as Record<string, unknown>
|
||||
} else {
|
||||
tabSiblingData = siblingData
|
||||
tabSiblingDoc = siblingDoc
|
||||
}
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: tabSiblingData,
|
||||
siblingDoc: tabSiblingDoc,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
doc,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,10 @@ import type { PayloadRequest, Where } from '../../types/index.js'
|
||||
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { combineQueries } from '../../database/combineQueries.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths.js'
|
||||
import {
|
||||
buildVersionGlobalFields,
|
||||
type GlobalSlug,
|
||||
type SanitizedGlobalConfig,
|
||||
} from '../../index.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateSearchParams.js'
|
||||
import { type GlobalSlug, type SanitizedGlobalConfig } from '../../index.js'
|
||||
import { killTransaction } from '../../utilities/killTransaction.js'
|
||||
import { buildVersionGlobalFields } from '../../versions/buildGlobalFields.js'
|
||||
|
||||
export type Arguments = {
|
||||
disableErrors?: boolean
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { SanitizedGlobalConfig } from '../config/types.js'
|
||||
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { combineQueries } from '../../database/combineQueries.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths.js'
|
||||
import { validateQueryPaths } from '../../database/queryValidation/validateSearchParams.js'
|
||||
import { afterRead } from '../../fields/hooks/afterRead/index.js'
|
||||
import { killTransaction } from '../../utilities/killTransaction.js'
|
||||
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields.js'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { GlobalSlug, Payload, RequestContext, TypedLocale } from '../../../index.js'
|
||||
import type { Document, PayloadRequest, Where } from '../../../types/index.js'
|
||||
|
||||
import { APIError } from '../../../errors/index.js'
|
||||
import { APIError } from '../../../errors/APIError.js'
|
||||
import { createLocalReq } from '../../../utilities/createLocalReq.js'
|
||||
import { countGlobalVersionsOperation } from '../countGlobalVersions.js'
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ import { afterChange } from '../../fields/hooks/afterChange/index.js'
|
||||
import { afterRead } from '../../fields/hooks/afterRead/index.js'
|
||||
import { beforeChange } from '../../fields/hooks/beforeChange/index.js'
|
||||
import { beforeValidate } from '../../fields/hooks/beforeValidate/index.js'
|
||||
import { deepCopyObjectSimple } from '../../index.js'
|
||||
import { checkDocumentLockStatus } from '../../utilities/checkDocumentLockStatus.js'
|
||||
import { commitTransaction } from '../../utilities/commitTransaction.js'
|
||||
import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js'
|
||||
import { getSelectMode } from '../../utilities/getSelectMode.js'
|
||||
import { initTransaction } from '../../utilities/initTransaction.js'
|
||||
import { killTransaction } from '../../utilities/killTransaction.js'
|
||||
|
||||
@@ -1006,8 +1006,10 @@ export { readMigrationFiles } from './database/migrations/readMigrationFiles.js'
|
||||
export { writeMigrationIndex } from './database/migrations/writeMigrationIndex.js'
|
||||
export type * from './database/queryValidation/types.js'
|
||||
export type { EntityPolicies, PathToQuery } from './database/queryValidation/types.js'
|
||||
export { validateQueryPaths } from './database/queryValidation/validateQueryPaths.js'
|
||||
export { validateSearchParam } from './database/queryValidation/validateSearchParams.js'
|
||||
export {
|
||||
validateQueryPaths,
|
||||
validateSearchParam,
|
||||
} from './database/queryValidation/validateSearchParams.js'
|
||||
export type {
|
||||
BaseDatabaseAdapter,
|
||||
BeginTransaction,
|
||||
@@ -1200,8 +1202,10 @@ export type {
|
||||
|
||||
export { getDefaultValue } from './fields/getDefaultValue.js'
|
||||
export { traverseFields as afterChangeTraverseFields } from './fields/hooks/afterChange/traverseFields.js'
|
||||
export { promise as afterReadPromise } from './fields/hooks/afterRead/promise.js'
|
||||
export { traverseFields as afterReadTraverseFields } from './fields/hooks/afterRead/traverseFields.js'
|
||||
export {
|
||||
promise as afterReadPromise,
|
||||
traverseFields as afterReadTraverseFields,
|
||||
} from './fields/hooks/afterRead/traverseFields.js'
|
||||
export { traverseFields as beforeChangeTraverseFields } from './fields/hooks/beforeChange/traverseFields.js'
|
||||
export { traverseFields as beforeValidateTraverseFields } from './fields/hooks/beforeValidate/traverseFields.js'
|
||||
export { default as sortableFieldTypes } from './fields/sortableFieldTypes.js'
|
||||
@@ -1298,12 +1302,7 @@ export * from './types/index.js'
|
||||
export { getFileByPath } from './uploads/getFileByPath.js'
|
||||
export type * from './uploads/types.js'
|
||||
export { commitTransaction } from './utilities/commitTransaction.js'
|
||||
export {
|
||||
configToJSONSchema,
|
||||
entityToJSONSchema,
|
||||
fieldsToJSONSchema,
|
||||
withNullableJSONSchemaType,
|
||||
} from './utilities/configToJSONSchema.js'
|
||||
export { configToJSONSchema, entityToJSONSchema } from './utilities/configToJSONSchema.js'
|
||||
export { createArrayFromCommaDelineated } from './utilities/createArrayFromCommaDelineated.js'
|
||||
export { createLocalReq } from './utilities/createLocalReq.js'
|
||||
export {
|
||||
@@ -1322,13 +1321,14 @@ export {
|
||||
type CustomVersionParser,
|
||||
} from './utilities/dependencies/dependencyChecker.js'
|
||||
export { getDependencies } from './utilities/dependencies/getDependencies.js'
|
||||
export { fieldsToJSONSchema, withNullableJSONSchemaType } from './utilities/fieldsToJSONSchema.js'
|
||||
export {
|
||||
findUp,
|
||||
findUpSync,
|
||||
pathExistsAndIsAccessible,
|
||||
pathExistsAndIsAccessibleSync,
|
||||
} from './utilities/findUp.js'
|
||||
export { default as flattenTopLevelFields } from './utilities/flattenTopLevelFields.js'
|
||||
export { flattenTopLevelFields } from './utilities/flattenTopLevelFields.js'
|
||||
export { formatErrors } from './utilities/formatErrors.js'
|
||||
export { formatLabels, formatNames, toWords } from './utilities/formatLabels.js'
|
||||
export { getCollectionIDFieldTypes } from './utilities/getCollectionIDFieldTypes.js'
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { JSONSchema4 } from 'json-schema'
|
||||
import type { SanitizedConfig } from '../../config/types.js'
|
||||
import type { JobsConfig } from './types/index.js'
|
||||
|
||||
import { fieldsToJSONSchema } from '../../utilities/configToJSONSchema.js'
|
||||
import { fieldsToJSONSchema } from '../../utilities/fieldsToJSONSchema.js'
|
||||
|
||||
export function generateJobsJSONSchemas(
|
||||
config: SanitizedConfig,
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import type { Payload, PayloadRequest, RunningJob, TypedJobs } from '../index.js'
|
||||
import type { RunningJobFromTask } from './config/types/workflowTypes.js'
|
||||
|
||||
import {
|
||||
createLocalReq,
|
||||
type Payload,
|
||||
type PayloadRequest,
|
||||
type RunningJob,
|
||||
type TypedJobs,
|
||||
} from '../index.js'
|
||||
import { createLocalReq } from '../utilities/createLocalReq.js'
|
||||
import { runJobs } from './operations/runJobs/index.js'
|
||||
|
||||
export const getJobsLocalAPI = (payload: Payload) => ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { JSONSchema4, JSONSchema4TypeName } from 'json-schema'
|
||||
import type { JSONSchema4 } from 'json-schema'
|
||||
|
||||
import pluralize from 'pluralize'
|
||||
const { singular } = pluralize
|
||||
@@ -6,55 +6,16 @@ const { singular } = pluralize
|
||||
import type { Auth } from '../auth/types.js'
|
||||
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
|
||||
import type { SanitizedConfig } from '../config/types.js'
|
||||
import type { Field, FieldAffectingData, Option } from '../fields/config/types.js'
|
||||
import type { Field, FieldAffectingData } from '../fields/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../errors/MissingEditorProp.js'
|
||||
import { fieldAffectsData, tabHasName } from '../fields/config/types.js'
|
||||
import { generateJobsJSONSchemas } from '../queues/config/generateJobsJSONSchemas.js'
|
||||
import { deepCopyObject } from './deepCopyObject.js'
|
||||
import { fieldsToJSONSchema } from './fieldsToJSONSchema.js'
|
||||
import { toWords } from './formatLabels.js'
|
||||
import { getCollectionIDFieldTypes } from './getCollectionIDFieldTypes.js'
|
||||
|
||||
const fieldIsRequired = (field: Field) => {
|
||||
const isConditional = Boolean(field?.admin && field?.admin?.condition)
|
||||
if (isConditional) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isMarkedRequired = 'required' in field && field.required === true
|
||||
if (fieldAffectsData(field) && isMarkedRequired) {
|
||||
return true
|
||||
}
|
||||
|
||||
// if any subfields are required, this field is required
|
||||
if ('fields' in field && field.type !== 'array') {
|
||||
return field.fields.some((subField) => fieldIsRequired(subField))
|
||||
}
|
||||
|
||||
// if any tab subfields have required fields, this field is required
|
||||
if (field.type === 'tabs') {
|
||||
return field.tabs.some((tab) => {
|
||||
if ('name' in tab) {
|
||||
return tab.fields.some((subField) => fieldIsRequired(subField))
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function buildOptionEnums(options: Option[]): string[] {
|
||||
return options.map((option) => {
|
||||
if (typeof option === 'object' && 'value' in option) {
|
||||
return option.value
|
||||
}
|
||||
|
||||
return option
|
||||
})
|
||||
}
|
||||
|
||||
function generateEntitySchemas(
|
||||
entities: (SanitizedCollectionConfig | SanitizedGlobalConfig)[],
|
||||
): JSONSchema4 {
|
||||
@@ -192,450 +153,6 @@ function generateDbEntitySchema(config: SanitizedConfig): JSONSchema4 {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSON Schema Type with 'null' added if the field is not required.
|
||||
*/
|
||||
export function withNullableJSONSchemaType(
|
||||
fieldType: JSONSchema4TypeName,
|
||||
isRequired: boolean,
|
||||
): JSONSchema4TypeName | JSONSchema4TypeName[] {
|
||||
const fieldTypes = [fieldType]
|
||||
if (isRequired) {
|
||||
return fieldType
|
||||
}
|
||||
fieldTypes.push('null')
|
||||
return fieldTypes
|
||||
}
|
||||
|
||||
export function fieldsToJSONSchema(
|
||||
/**
|
||||
* Used for relationship fields, to determine whether to use a string or number type for the ID.
|
||||
* While there is a default ID field type set by the db adapter, they can differ on a collection-level
|
||||
* if they have custom ID fields.
|
||||
*/
|
||||
collectionIDFieldTypes: { [key: string]: 'number' | 'string' },
|
||||
fields: Field[],
|
||||
/**
|
||||
* Allows you to define new top-level interfaces that can be re-used in the output schema.
|
||||
*/
|
||||
interfaceNameDefinitions: Map<string, JSONSchema4>,
|
||||
config?: SanitizedConfig,
|
||||
): {
|
||||
properties: {
|
||||
[k: string]: JSONSchema4
|
||||
}
|
||||
required: string[]
|
||||
} {
|
||||
const requiredFieldNames = new Set<string>()
|
||||
|
||||
return {
|
||||
properties: Object.fromEntries(
|
||||
fields.reduce((fieldSchemas, field) => {
|
||||
const isRequired = fieldAffectsData(field) && fieldIsRequired(field)
|
||||
if (isRequired) {
|
||||
requiredFieldNames.add(field.name)
|
||||
}
|
||||
|
||||
let fieldSchema: JSONSchema4
|
||||
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
...fieldsToJSONSchema(
|
||||
collectionIDFieldTypes,
|
||||
field.fields,
|
||||
interfaceNameDefinitions,
|
||||
config,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
if (field.interfaceName) {
|
||||
interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
|
||||
|
||||
fieldSchema = {
|
||||
$ref: `#/definitions/${field.interfaceName}`,
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'blocks': {
|
||||
// Check for a case where no blocks are provided.
|
||||
// We need to generate an empty array for this case, note that JSON schema 4 doesn't support empty arrays
|
||||
// so the best we can get is `unknown[]`
|
||||
const hasBlocks = Boolean(field.blocks.length)
|
||||
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: hasBlocks
|
||||
? {
|
||||
oneOf: field.blocks.map((block) => {
|
||||
const blockFieldSchemas = fieldsToJSONSchema(
|
||||
collectionIDFieldTypes,
|
||||
block.fields,
|
||||
interfaceNameDefinitions,
|
||||
config,
|
||||
)
|
||||
|
||||
const blockSchema: JSONSchema4 = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
...blockFieldSchemas.properties,
|
||||
blockType: {
|
||||
const: block.slug,
|
||||
},
|
||||
},
|
||||
required: ['blockType', ...blockFieldSchemas.required],
|
||||
}
|
||||
|
||||
if (block.interfaceName) {
|
||||
interfaceNameDefinitions.set(block.interfaceName, blockSchema)
|
||||
|
||||
return {
|
||||
$ref: `#/definitions/${block.interfaceName}`,
|
||||
}
|
||||
}
|
||||
|
||||
return blockSchema
|
||||
}),
|
||||
}
|
||||
: {},
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'checkbox': {
|
||||
fieldSchema = { type: withNullableJSONSchemaType('boolean', isRequired) }
|
||||
break
|
||||
}
|
||||
case 'code':
|
||||
case 'date':
|
||||
case 'email':
|
||||
case 'textarea': {
|
||||
fieldSchema = { type: withNullableJSONSchemaType('string', isRequired) }
|
||||
break
|
||||
}
|
||||
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
const childSchema = fieldsToJSONSchema(
|
||||
collectionIDFieldTypes,
|
||||
field.fields,
|
||||
interfaceNameDefinitions,
|
||||
config,
|
||||
)
|
||||
Object.entries(childSchema.properties).forEach(([propName, propSchema]) => {
|
||||
fieldSchemas.set(propName, propSchema)
|
||||
})
|
||||
childSchema.required.forEach((propName) => {
|
||||
requiredFieldNames.add(propName)
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'group': {
|
||||
fieldSchema = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
...fieldsToJSONSchema(
|
||||
collectionIDFieldTypes,
|
||||
field.fields,
|
||||
interfaceNameDefinitions,
|
||||
config,
|
||||
),
|
||||
}
|
||||
|
||||
if (field.interfaceName) {
|
||||
interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
|
||||
|
||||
fieldSchema = {
|
||||
$ref: `#/definitions/${field.interfaceName}`,
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'join': {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('object', false),
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
docs: {
|
||||
type: withNullableJSONSchemaType('array', false),
|
||||
items: {
|
||||
oneOf: [
|
||||
{
|
||||
type: collectionIDFieldTypes[field.collection],
|
||||
},
|
||||
{
|
||||
$ref: `#/definitions/${field.collection}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
hasNextPage: { type: withNullableJSONSchemaType('boolean', false) },
|
||||
},
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'json': {
|
||||
fieldSchema = field.jsonSchema?.schema || {
|
||||
type: ['object', 'array', 'string', 'number', 'boolean', 'null'],
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'number': {
|
||||
if (field.hasMany === true) {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: { type: 'number' },
|
||||
}
|
||||
} else {
|
||||
fieldSchema = { type: withNullableJSONSchemaType('number', isRequired) }
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'point': {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: [
|
||||
{
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
maxItems: 2,
|
||||
minItems: 2,
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'radio': {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('string', isRequired),
|
||||
enum: buildOptionEnums(field.options),
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
case 'upload': {
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
if (field.hasMany) {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: {
|
||||
oneOf: field.relationTo.map((relation) => {
|
||||
return {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
relationTo: {
|
||||
const: relation,
|
||||
},
|
||||
value: {
|
||||
oneOf: [
|
||||
{
|
||||
type: collectionIDFieldTypes[relation],
|
||||
},
|
||||
{
|
||||
$ref: `#/definitions/${relation}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ['value', 'relationTo'],
|
||||
}
|
||||
}),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
fieldSchema = {
|
||||
oneOf: field.relationTo.map((relation) => {
|
||||
return {
|
||||
type: withNullableJSONSchemaType('object', isRequired),
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
relationTo: {
|
||||
const: relation,
|
||||
},
|
||||
value: {
|
||||
oneOf: [
|
||||
{
|
||||
type: collectionIDFieldTypes[relation],
|
||||
},
|
||||
{
|
||||
$ref: `#/definitions/${relation}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ['value', 'relationTo'],
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
} else if (field.hasMany) {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: {
|
||||
oneOf: [
|
||||
{
|
||||
type: collectionIDFieldTypes[field.relationTo],
|
||||
},
|
||||
{
|
||||
$ref: `#/definitions/${field.relationTo}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
fieldSchema = {
|
||||
oneOf: [
|
||||
{
|
||||
type: withNullableJSONSchemaType(
|
||||
collectionIDFieldTypes[field.relationTo],
|
||||
isRequired,
|
||||
),
|
||||
},
|
||||
{
|
||||
$ref: `#/definitions/${field.relationTo}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
if (field.editor.outputSchema) {
|
||||
fieldSchema = field.editor.outputSchema({
|
||||
collectionIDFieldTypes,
|
||||
config,
|
||||
field,
|
||||
interfaceNameDefinitions,
|
||||
isRequired,
|
||||
})
|
||||
} else {
|
||||
// Maintain backwards compatibility with existing rich text editors
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: {
|
||||
type: 'object',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'select': {
|
||||
const optionEnums = buildOptionEnums(field.options)
|
||||
|
||||
if (field.hasMany) {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
}
|
||||
if (optionEnums?.length) {
|
||||
;(fieldSchema.items as JSONSchema4).enum = optionEnums
|
||||
}
|
||||
} else {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('string', isRequired),
|
||||
}
|
||||
if (optionEnums?.length) {
|
||||
fieldSchema.enum = optionEnums
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
field.tabs.forEach((tab) => {
|
||||
const childSchema = fieldsToJSONSchema(
|
||||
collectionIDFieldTypes,
|
||||
tab.fields,
|
||||
interfaceNameDefinitions,
|
||||
config,
|
||||
)
|
||||
if (tabHasName(tab)) {
|
||||
// could have interface
|
||||
fieldSchemas.set(tab.name, {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
...childSchema,
|
||||
})
|
||||
|
||||
// If the named tab has any required fields then we mark this as required otherwise it should be optional
|
||||
const hasRequiredFields = tab.fields.some((subField) => fieldIsRequired(subField))
|
||||
|
||||
if (hasRequiredFields) {
|
||||
requiredFieldNames.add(tab.name)
|
||||
}
|
||||
} else {
|
||||
Object.entries(childSchema.properties).forEach(([propName, propSchema]) => {
|
||||
fieldSchemas.set(propName, propSchema)
|
||||
})
|
||||
childSchema.required.forEach((propName) => {
|
||||
requiredFieldNames.add(propName)
|
||||
})
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'text':
|
||||
if (field.hasMany === true) {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: { type: 'string' },
|
||||
}
|
||||
} else {
|
||||
fieldSchema = { type: withNullableJSONSchemaType('string', isRequired) }
|
||||
}
|
||||
break
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ('typescriptSchema' in field && field?.typescriptSchema?.length) {
|
||||
for (const schema of field.typescriptSchema) {
|
||||
fieldSchema = schema({ jsonSchema: fieldSchema })
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldSchema && fieldAffectsData(field)) {
|
||||
fieldSchemas.set(field.name, fieldSchema)
|
||||
}
|
||||
|
||||
return fieldSchemas
|
||||
}, new Map<string, JSONSchema4>()),
|
||||
),
|
||||
required: Array.from(requiredFieldNames),
|
||||
}
|
||||
}
|
||||
|
||||
// This function is part of the public API and is exported through payload/utilities
|
||||
export function entityToJSONSchema(
|
||||
config: SanitizedConfig,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { getDependencies } from '../../index.js'
|
||||
import { getDependencies } from './getDependencies.js'
|
||||
import { compareVersions } from './versionUtils.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
|
||||
489
packages/payload/src/utilities/fieldsToJSONSchema.ts
Normal file
489
packages/payload/src/utilities/fieldsToJSONSchema.ts
Normal file
@@ -0,0 +1,489 @@
|
||||
import type { JSONSchema4, JSONSchema4TypeName } from 'json-schema'
|
||||
|
||||
import type { SanitizedConfig } from '../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../errors/MissingEditorProp.js'
|
||||
import { type Field, fieldAffectsData, type Option, tabHasName } from '../fields/config/types.js'
|
||||
|
||||
/**
|
||||
* Returns a JSON Schema Type with 'null' added if the field is not required.
|
||||
*/
|
||||
export function withNullableJSONSchemaType(
|
||||
fieldType: JSONSchema4TypeName,
|
||||
isRequired: boolean,
|
||||
): JSONSchema4TypeName | JSONSchema4TypeName[] {
|
||||
const fieldTypes = [fieldType]
|
||||
if (isRequired) {
|
||||
return fieldType
|
||||
}
|
||||
fieldTypes.push('null')
|
||||
return fieldTypes
|
||||
}
|
||||
|
||||
const fieldIsRequired = (field: Field) => {
|
||||
const isConditional = Boolean(field?.admin && field?.admin?.condition)
|
||||
if (isConditional) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isMarkedRequired = 'required' in field && field.required === true
|
||||
if (fieldAffectsData(field) && isMarkedRequired) {
|
||||
return true
|
||||
}
|
||||
|
||||
// if any subfields are required, this field is required
|
||||
if ('fields' in field && field.type !== 'array') {
|
||||
return field.fields.some((subField) => fieldIsRequired(subField))
|
||||
}
|
||||
|
||||
// if any tab subfields have required fields, this field is required
|
||||
if (field.type === 'tabs') {
|
||||
return field.tabs.some((tab) => {
|
||||
if ('name' in tab) {
|
||||
return tab.fields.some((subField) => fieldIsRequired(subField))
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function buildOptionEnums(options: Option[]): string[] {
|
||||
return options.map((option) => {
|
||||
if (typeof option === 'object' && 'value' in option) {
|
||||
return option.value
|
||||
}
|
||||
|
||||
return option
|
||||
})
|
||||
}
|
||||
|
||||
export function fieldsToJSONSchema(
|
||||
/**
|
||||
* Used for relationship fields, to determine whether to use a string or number type for the ID.
|
||||
* While there is a default ID field type set by the db adapter, they can differ on a collection-level
|
||||
* if they have custom ID fields.
|
||||
*/
|
||||
collectionIDFieldTypes: { [key: string]: 'number' | 'string' },
|
||||
fields: Field[],
|
||||
/**
|
||||
* Allows you to define new top-level interfaces that can be re-used in the output schema.
|
||||
*/
|
||||
interfaceNameDefinitions: Map<string, JSONSchema4>,
|
||||
config?: SanitizedConfig,
|
||||
): {
|
||||
properties: {
|
||||
[k: string]: JSONSchema4
|
||||
}
|
||||
required: string[]
|
||||
} {
|
||||
const requiredFieldNames = new Set<string>()
|
||||
|
||||
return {
|
||||
properties: Object.fromEntries(
|
||||
fields.reduce((fieldSchemas, field) => {
|
||||
const isRequired = fieldAffectsData(field) && fieldIsRequired(field)
|
||||
if (isRequired) {
|
||||
requiredFieldNames.add(field.name)
|
||||
}
|
||||
|
||||
let fieldSchema: JSONSchema4
|
||||
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
...fieldsToJSONSchema(
|
||||
collectionIDFieldTypes,
|
||||
field.fields,
|
||||
interfaceNameDefinitions,
|
||||
config,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
if (field.interfaceName) {
|
||||
interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
|
||||
|
||||
fieldSchema = {
|
||||
$ref: `#/definitions/${field.interfaceName}`,
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'blocks': {
|
||||
// Check for a case where no blocks are provided.
|
||||
// We need to generate an empty array for this case, note that JSON schema 4 doesn't support empty arrays
|
||||
// so the best we can get is `unknown[]`
|
||||
const hasBlocks = Boolean(field.blocks.length)
|
||||
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: hasBlocks
|
||||
? {
|
||||
oneOf: field.blocks.map((block) => {
|
||||
const blockFieldSchemas = fieldsToJSONSchema(
|
||||
collectionIDFieldTypes,
|
||||
block.fields,
|
||||
interfaceNameDefinitions,
|
||||
config,
|
||||
)
|
||||
|
||||
const blockSchema: JSONSchema4 = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
...blockFieldSchemas.properties,
|
||||
blockType: {
|
||||
const: block.slug,
|
||||
},
|
||||
},
|
||||
required: ['blockType', ...blockFieldSchemas.required],
|
||||
}
|
||||
|
||||
if (block.interfaceName) {
|
||||
interfaceNameDefinitions.set(block.interfaceName, blockSchema)
|
||||
|
||||
return {
|
||||
$ref: `#/definitions/${block.interfaceName}`,
|
||||
}
|
||||
}
|
||||
|
||||
return blockSchema
|
||||
}),
|
||||
}
|
||||
: {},
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'checkbox': {
|
||||
fieldSchema = { type: withNullableJSONSchemaType('boolean', isRequired) }
|
||||
break
|
||||
}
|
||||
case 'code':
|
||||
case 'date':
|
||||
case 'email':
|
||||
case 'textarea': {
|
||||
fieldSchema = { type: withNullableJSONSchemaType('string', isRequired) }
|
||||
break
|
||||
}
|
||||
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
const childSchema = fieldsToJSONSchema(
|
||||
collectionIDFieldTypes,
|
||||
field.fields,
|
||||
interfaceNameDefinitions,
|
||||
config,
|
||||
)
|
||||
Object.entries(childSchema.properties).forEach(([propName, propSchema]) => {
|
||||
fieldSchemas.set(propName, propSchema)
|
||||
})
|
||||
childSchema.required.forEach((propName) => {
|
||||
requiredFieldNames.add(propName)
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'group': {
|
||||
fieldSchema = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
...fieldsToJSONSchema(
|
||||
collectionIDFieldTypes,
|
||||
field.fields,
|
||||
interfaceNameDefinitions,
|
||||
config,
|
||||
),
|
||||
}
|
||||
|
||||
if (field.interfaceName) {
|
||||
interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
|
||||
|
||||
fieldSchema = {
|
||||
$ref: `#/definitions/${field.interfaceName}`,
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'join': {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('object', false),
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
docs: {
|
||||
type: withNullableJSONSchemaType('array', false),
|
||||
items: {
|
||||
oneOf: [
|
||||
{
|
||||
type: collectionIDFieldTypes[field.collection],
|
||||
},
|
||||
{
|
||||
$ref: `#/definitions/${field.collection}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
hasNextPage: { type: withNullableJSONSchemaType('boolean', false) },
|
||||
},
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'json': {
|
||||
fieldSchema = field.jsonSchema?.schema || {
|
||||
type: ['object', 'array', 'string', 'number', 'boolean', 'null'],
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'number': {
|
||||
if (field.hasMany === true) {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: { type: 'number' },
|
||||
}
|
||||
} else {
|
||||
fieldSchema = { type: withNullableJSONSchemaType('number', isRequired) }
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'point': {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: [
|
||||
{
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
maxItems: 2,
|
||||
minItems: 2,
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'radio': {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('string', isRequired),
|
||||
enum: buildOptionEnums(field.options),
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
case 'upload': {
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
if (field.hasMany) {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: {
|
||||
oneOf: field.relationTo.map((relation) => {
|
||||
return {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
relationTo: {
|
||||
const: relation,
|
||||
},
|
||||
value: {
|
||||
oneOf: [
|
||||
{
|
||||
type: collectionIDFieldTypes[relation],
|
||||
},
|
||||
{
|
||||
$ref: `#/definitions/${relation}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ['value', 'relationTo'],
|
||||
}
|
||||
}),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
fieldSchema = {
|
||||
oneOf: field.relationTo.map((relation) => {
|
||||
return {
|
||||
type: withNullableJSONSchemaType('object', isRequired),
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
relationTo: {
|
||||
const: relation,
|
||||
},
|
||||
value: {
|
||||
oneOf: [
|
||||
{
|
||||
type: collectionIDFieldTypes[relation],
|
||||
},
|
||||
{
|
||||
$ref: `#/definitions/${relation}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ['value', 'relationTo'],
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
} else if (field.hasMany) {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: {
|
||||
oneOf: [
|
||||
{
|
||||
type: collectionIDFieldTypes[field.relationTo],
|
||||
},
|
||||
{
|
||||
$ref: `#/definitions/${field.relationTo}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
fieldSchema = {
|
||||
oneOf: [
|
||||
{
|
||||
type: withNullableJSONSchemaType(
|
||||
collectionIDFieldTypes[field.relationTo],
|
||||
isRequired,
|
||||
),
|
||||
},
|
||||
{
|
||||
$ref: `#/definitions/${field.relationTo}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
if (typeof field.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
if (field.editor.outputSchema) {
|
||||
fieldSchema = field.editor.outputSchema({
|
||||
collectionIDFieldTypes,
|
||||
config,
|
||||
field,
|
||||
interfaceNameDefinitions,
|
||||
isRequired,
|
||||
})
|
||||
} else {
|
||||
// Maintain backwards compatibility with existing rich text editors
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: {
|
||||
type: 'object',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'select': {
|
||||
const optionEnums = buildOptionEnums(field.options)
|
||||
|
||||
if (field.hasMany) {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
}
|
||||
if (optionEnums?.length) {
|
||||
;(fieldSchema.items as JSONSchema4).enum = optionEnums
|
||||
}
|
||||
} else {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('string', isRequired),
|
||||
}
|
||||
if (optionEnums?.length) {
|
||||
fieldSchema.enum = optionEnums
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
field.tabs.forEach((tab) => {
|
||||
const childSchema = fieldsToJSONSchema(
|
||||
collectionIDFieldTypes,
|
||||
tab.fields,
|
||||
interfaceNameDefinitions,
|
||||
config,
|
||||
)
|
||||
if (tabHasName(tab)) {
|
||||
// could have interface
|
||||
fieldSchemas.set(tab.name, {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
...childSchema,
|
||||
})
|
||||
|
||||
// If the named tab has any required fields then we mark this as required otherwise it should be optional
|
||||
const hasRequiredFields = tab.fields.some((subField) => fieldIsRequired(subField))
|
||||
|
||||
if (hasRequiredFields) {
|
||||
requiredFieldNames.add(tab.name)
|
||||
}
|
||||
} else {
|
||||
Object.entries(childSchema.properties).forEach(([propName, propSchema]) => {
|
||||
fieldSchemas.set(propName, propSchema)
|
||||
})
|
||||
childSchema.required.forEach((propName) => {
|
||||
requiredFieldNames.add(propName)
|
||||
})
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'text':
|
||||
if (field.hasMany === true) {
|
||||
fieldSchema = {
|
||||
type: withNullableJSONSchemaType('array', isRequired),
|
||||
items: { type: 'string' },
|
||||
}
|
||||
} else {
|
||||
fieldSchema = { type: withNullableJSONSchemaType('string', isRequired) }
|
||||
}
|
||||
break
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ('typescriptSchema' in field && field?.typescriptSchema?.length) {
|
||||
for (const schema of field.typescriptSchema) {
|
||||
fieldSchema = schema({ jsonSchema: fieldSchema })
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldSchema && fieldAffectsData(field)) {
|
||||
fieldSchemas.set(field.name, fieldSchema)
|
||||
}
|
||||
|
||||
return fieldSchemas
|
||||
}, new Map<string, JSONSchema4>()),
|
||||
),
|
||||
required: Array.from(requiredFieldNames),
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ type TabType<TField> = TField extends ClientField ? ClientTab : Tab
|
||||
* @param fields
|
||||
* @param keepPresentationalFields if true, will skip flattening fields that are presentational only
|
||||
*/
|
||||
function flattenFields<TField extends ClientField | Field>(
|
||||
export function flattenTopLevelFields<TField extends ClientField | Field>(
|
||||
fields: TField[],
|
||||
keepPresentationalFields?: boolean,
|
||||
): FlattenedField<TField>[] {
|
||||
@@ -39,7 +39,7 @@ function flattenFields<TField extends ClientField | Field>(
|
||||
}
|
||||
|
||||
if (fieldHasSubFields(field)) {
|
||||
return [...fieldsToUse, ...flattenFields(field.fields as TField[], keepPresentationalFields)]
|
||||
return [...fieldsToUse, ...flattenTopLevelFields(field.fields as TField[], keepPresentationalFields)]
|
||||
}
|
||||
|
||||
if (field.type === 'tabs' && 'tabs' in field) {
|
||||
@@ -51,7 +51,7 @@ function flattenFields<TField extends ClientField | Field>(
|
||||
} else {
|
||||
return [
|
||||
...tabFields,
|
||||
...flattenFields(tab.fields as TField[], keepPresentationalFields),
|
||||
...flattenTopLevelFields(tab.fields as TField[], keepPresentationalFields),
|
||||
]
|
||||
}
|
||||
}, []),
|
||||
@@ -61,5 +61,3 @@ function flattenFields<TField extends ClientField | Field>(
|
||||
return fieldsToUse
|
||||
}, [])
|
||||
}
|
||||
|
||||
export default flattenFields
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
import type { Payload } from '../index.js'
|
||||
import type { PayloadRequest, SelectType } from '../types/index.js'
|
||||
|
||||
import { deepCopyObjectSimple } from '../index.js'
|
||||
import { deepCopyObjectSimple } from '../utilities/deepCopyObject.js'
|
||||
import sanitizeInternalFields from '../utilities/sanitizeInternalFields.js'
|
||||
import { getQueryDraftsSelect } from './drafts/getQueryDraftsSelect.js'
|
||||
import { enforceMaxVersions } from './enforceMaxVersions.js'
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
import type { UnknownConvertedNodeData } from './index.js'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import type { UnknownConvertedNodeData } from './types.js'
|
||||
|
||||
type Props = {
|
||||
data: UnknownConvertedNodeData
|
||||
}
|
||||
|
||||
@@ -5,10 +5,7 @@ import { addClassNamesToElement } from '@lexical/utils'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import * as React from 'react'
|
||||
|
||||
export type UnknownConvertedNodeData = {
|
||||
nodeData: unknown
|
||||
nodeType: string
|
||||
}
|
||||
import type { UnknownConvertedNodeData } from './types.js'
|
||||
|
||||
export type SerializedUnknownConvertedNode = Spread<
|
||||
{
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export type UnknownConvertedNodeData = {
|
||||
nodeData: unknown
|
||||
nodeType: string
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
import type { UnknownConvertedNodeData } from './index.js'
|
||||
|
||||
import './index.scss'
|
||||
import type { UnknownConvertedNodeData } from './types.js'
|
||||
|
||||
type Props = {
|
||||
data: UnknownConvertedNodeData
|
||||
|
||||
@@ -5,10 +5,7 @@ import { addClassNamesToElement } from '@lexical/utils'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import * as React from 'react'
|
||||
|
||||
export type UnknownConvertedNodeData = {
|
||||
nodeData: unknown
|
||||
nodeType: string
|
||||
}
|
||||
import type { UnknownConvertedNodeData } from './types.js'
|
||||
|
||||
export type SerializedUnknownConvertedNode = Spread<
|
||||
{
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export type UnknownConvertedNodeData = {
|
||||
nodeData: unknown
|
||||
nodeType: string
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { Field, PayloadRequest, PopulateType } from 'payload'
|
||||
import type { Field, PayloadRequest, PopulateType, RichTextAdapter, RichTextField } from 'payload'
|
||||
|
||||
import { fieldAffectsData, fieldHasSubFields, fieldIsArrayType, tabHasName } from 'payload/shared'
|
||||
|
||||
import type { AdapterArguments } from '../types.js'
|
||||
|
||||
import { populate } from './populate.js'
|
||||
import { recurseRichText } from './richTextRelationshipPromise.js'
|
||||
|
||||
type NestedRichTextFieldsArgs = {
|
||||
currentDepth?: number
|
||||
@@ -227,3 +228,168 @@ export const recurseNestedFields = ({
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export type Args = Parameters<
|
||||
RichTextAdapter<any[], AdapterArguments>['graphQLPopulationPromises']
|
||||
>[0]
|
||||
|
||||
type RecurseRichTextArgs = {
|
||||
children: unknown[]
|
||||
currentDepth: number
|
||||
depth: number
|
||||
draft: boolean
|
||||
field: RichTextField<any[], any, any>
|
||||
overrideAccess: boolean
|
||||
populateArg?: PopulateType
|
||||
populationPromises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
showHiddenFields: boolean
|
||||
}
|
||||
|
||||
export const recurseRichText = ({
|
||||
children,
|
||||
currentDepth = 0,
|
||||
depth,
|
||||
draft,
|
||||
field,
|
||||
overrideAccess = false,
|
||||
populateArg,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
}: RecurseRichTextArgs): void => {
|
||||
if (depth <= 0 || currentDepth > depth) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(children)) {
|
||||
;(children as any[]).forEach((element) => {
|
||||
if ((element.type === 'relationship' || element.type === 'upload') && element?.value?.id) {
|
||||
const collection = req.payload.collections[element?.relationTo]
|
||||
|
||||
if (collection) {
|
||||
populationPromises.push(
|
||||
populate({
|
||||
id: element.value.id,
|
||||
collection,
|
||||
currentDepth,
|
||||
data: element,
|
||||
depth,
|
||||
draft,
|
||||
field,
|
||||
key: 'value',
|
||||
overrideAccess,
|
||||
req,
|
||||
select:
|
||||
req.payloadAPI !== 'GraphQL'
|
||||
? (populateArg?.[collection.config.slug] ?? collection.config.defaultPopulate)
|
||||
: undefined,
|
||||
showHiddenFields,
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (
|
||||
element.type === 'upload' &&
|
||||
Array.isArray(field.admin?.upload?.collections?.[element?.relationTo]?.fields)
|
||||
) {
|
||||
recurseNestedFields({
|
||||
currentDepth,
|
||||
data: element.fields || {},
|
||||
depth,
|
||||
draft,
|
||||
fields: field.admin.upload.collections[element.relationTo].fields,
|
||||
overrideAccess,
|
||||
populateArg,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (element.type === 'link') {
|
||||
if (element?.doc?.value && element?.doc?.relationTo) {
|
||||
const collection = req.payload.collections[element?.doc?.relationTo]
|
||||
|
||||
if (collection) {
|
||||
populationPromises.push(
|
||||
populate({
|
||||
id: element.doc.value,
|
||||
collection,
|
||||
currentDepth,
|
||||
data: element.doc,
|
||||
depth,
|
||||
draft,
|
||||
field,
|
||||
key: 'value',
|
||||
overrideAccess,
|
||||
req,
|
||||
select:
|
||||
req.payloadAPI !== 'GraphQL'
|
||||
? (populateArg?.[collection.config.slug] ?? collection.config.defaultPopulate)
|
||||
: undefined,
|
||||
showHiddenFields,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(field.admin?.link?.fields)) {
|
||||
recurseNestedFields({
|
||||
currentDepth,
|
||||
data: element.fields || {},
|
||||
depth,
|
||||
draft,
|
||||
fields: field.admin?.link?.fields,
|
||||
overrideAccess,
|
||||
populateArg,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (element?.children) {
|
||||
recurseRichText({
|
||||
children: element.children,
|
||||
currentDepth,
|
||||
depth,
|
||||
draft,
|
||||
field,
|
||||
overrideAccess,
|
||||
populateArg,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const richTextRelationshipPromise = ({
|
||||
currentDepth,
|
||||
depth,
|
||||
draft,
|
||||
field,
|
||||
overrideAccess,
|
||||
populateArg,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
}: Args) => {
|
||||
recurseRichText({
|
||||
children: siblingDoc[field.name] as unknown[],
|
||||
currentDepth,
|
||||
depth,
|
||||
draft,
|
||||
field,
|
||||
overrideAccess,
|
||||
populateArg,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
import type {
|
||||
CollectionConfig,
|
||||
PayloadRequest,
|
||||
PopulateType,
|
||||
RichTextAdapter,
|
||||
RichTextField,
|
||||
} from 'payload'
|
||||
|
||||
import type { AdapterArguments } from '../types.js'
|
||||
|
||||
import { populate } from './populate.js'
|
||||
import { recurseNestedFields } from './recurseNestedFields.js'
|
||||
|
||||
export type Args = Parameters<
|
||||
RichTextAdapter<any[], AdapterArguments>['graphQLPopulationPromises']
|
||||
>[0]
|
||||
|
||||
type RecurseRichTextArgs = {
|
||||
children: unknown[]
|
||||
currentDepth: number
|
||||
depth: number
|
||||
draft: boolean
|
||||
field: RichTextField<any[], any, any>
|
||||
overrideAccess: boolean
|
||||
populateArg?: PopulateType
|
||||
populationPromises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
showHiddenFields: boolean
|
||||
}
|
||||
|
||||
export const recurseRichText = ({
|
||||
children,
|
||||
currentDepth = 0,
|
||||
depth,
|
||||
draft,
|
||||
field,
|
||||
overrideAccess = false,
|
||||
populateArg,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
}: RecurseRichTextArgs): void => {
|
||||
if (depth <= 0 || currentDepth > depth) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(children)) {
|
||||
;(children as any[]).forEach((element) => {
|
||||
if ((element.type === 'relationship' || element.type === 'upload') && element?.value?.id) {
|
||||
const collection = req.payload.collections[element?.relationTo]
|
||||
|
||||
if (collection) {
|
||||
populationPromises.push(
|
||||
populate({
|
||||
id: element.value.id,
|
||||
collection,
|
||||
currentDepth,
|
||||
data: element,
|
||||
depth,
|
||||
draft,
|
||||
field,
|
||||
key: 'value',
|
||||
overrideAccess,
|
||||
req,
|
||||
select:
|
||||
req.payloadAPI !== 'GraphQL'
|
||||
? (populateArg?.[collection.config.slug] ?? collection.config.defaultPopulate)
|
||||
: undefined,
|
||||
showHiddenFields,
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (
|
||||
element.type === 'upload' &&
|
||||
Array.isArray(field.admin?.upload?.collections?.[element?.relationTo]?.fields)
|
||||
) {
|
||||
recurseNestedFields({
|
||||
currentDepth,
|
||||
data: element.fields || {},
|
||||
depth,
|
||||
draft,
|
||||
fields: field.admin.upload.collections[element.relationTo].fields,
|
||||
overrideAccess,
|
||||
populateArg,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (element.type === 'link') {
|
||||
if (element?.doc?.value && element?.doc?.relationTo) {
|
||||
const collection = req.payload.collections[element?.doc?.relationTo]
|
||||
|
||||
if (collection) {
|
||||
populationPromises.push(
|
||||
populate({
|
||||
id: element.doc.value,
|
||||
collection,
|
||||
currentDepth,
|
||||
data: element.doc,
|
||||
depth,
|
||||
draft,
|
||||
field,
|
||||
key: 'value',
|
||||
overrideAccess,
|
||||
req,
|
||||
select:
|
||||
req.payloadAPI !== 'GraphQL'
|
||||
? (populateArg?.[collection.config.slug] ?? collection.config.defaultPopulate)
|
||||
: undefined,
|
||||
showHiddenFields,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(field.admin?.link?.fields)) {
|
||||
recurseNestedFields({
|
||||
currentDepth,
|
||||
data: element.fields || {},
|
||||
depth,
|
||||
draft,
|
||||
fields: field.admin?.link?.fields,
|
||||
overrideAccess,
|
||||
populateArg,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (element?.children) {
|
||||
recurseRichText({
|
||||
children: element.children,
|
||||
currentDepth,
|
||||
depth,
|
||||
draft,
|
||||
field,
|
||||
overrideAccess,
|
||||
populateArg,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const richTextRelationshipPromise = ({
|
||||
currentDepth,
|
||||
depth,
|
||||
draft,
|
||||
field,
|
||||
overrideAccess,
|
||||
populateArg,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
}: Args) => {
|
||||
recurseRichText({
|
||||
children: siblingDoc[field.name] as unknown[],
|
||||
currentDepth,
|
||||
depth,
|
||||
draft,
|
||||
field,
|
||||
overrideAccess,
|
||||
populateArg,
|
||||
populationPromises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
})
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { sanitizeFields, withNullableJSONSchemaType } from 'payload'
|
||||
|
||||
import type { AdapterArguments } from './types.js'
|
||||
|
||||
import { richTextRelationshipPromise } from './data/richTextRelationshipPromise.js'
|
||||
import { richTextRelationshipPromise } from './data/recurseNestedFields.js'
|
||||
import { richTextValidate } from './data/validation.js'
|
||||
import { elements as elementTypes } from './field/elements/index.js'
|
||||
import { transformExtraFields } from './field/elements/link/utilities.js'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Locale } from 'date-fns'
|
||||
|
||||
import type { clientTranslationKeys } from './clientKeys.js'
|
||||
import type { enTranslations } from './languages/en.js'
|
||||
import type { acceptedLanguages } from './utilities/languages.js'
|
||||
|
||||
@@ -110,8 +109,7 @@ export type DefaultTranslationKeysUnSanitized = NestedKeysUnSanitized<DefaultTra
|
||||
*/
|
||||
export type DefaultTranslationKeys = NestedKeysStripped<DefaultTranslationsObject>
|
||||
|
||||
export type ClientTranslationKeys<TExtraProps = (typeof clientTranslationKeys)[number]> =
|
||||
TExtraProps
|
||||
export type ClientTranslationKeys<TExtraProps = DefaultTranslationKeys> = TExtraProps
|
||||
|
||||
// Use GenericTranslationsObject instead of reconstructing the object from the client keys. This is because reconstructing the object is
|
||||
// A) Expensive on performance.
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import type { RenderFieldMethod } from './types.js'
|
||||
|
||||
import { getFilterOptionsQuery } from './getFilterOptionsQuery.js'
|
||||
import { iterateFields } from './iterateFields.js'
|
||||
|
||||
const ObjectId = (ObjectIdImport.default ||
|
||||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
|
||||
@@ -736,3 +735,137 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type IterateFieldsArgs = {
|
||||
addErrorPathToParent: (fieldPath: string) => void
|
||||
/**
|
||||
* if any parents is localized, then the field is localized. @default false
|
||||
*/
|
||||
anyParentLocalized?: boolean
|
||||
collectionSlug?: string
|
||||
data: Data
|
||||
fields: Field[]
|
||||
fieldSchemaMap: FieldSchemaMap
|
||||
filter?: (args: AddFieldStatePromiseArgs) => boolean
|
||||
/**
|
||||
* Force the value of fields like arrays or blocks to be the full value instead of the length @default false
|
||||
*/
|
||||
forceFullValue?: boolean
|
||||
fullData: Data
|
||||
id?: number | string
|
||||
/**
|
||||
* Whether the field schema should be included in the state. @default false
|
||||
*/
|
||||
includeSchema?: boolean
|
||||
/**
|
||||
* Whether to omit parent fields in the state. @default false
|
||||
*/
|
||||
omitParents?: boolean
|
||||
/**
|
||||
* operation is only needed for validation
|
||||
*/
|
||||
operation: 'create' | 'update'
|
||||
parentIndexPath: string
|
||||
parentPassesCondition?: boolean
|
||||
parentPath: string
|
||||
parentSchemaPath: string
|
||||
permissions:
|
||||
| {
|
||||
[fieldName: string]: SanitizedFieldPermissions
|
||||
}
|
||||
| null
|
||||
| SanitizedFieldPermissions
|
||||
preferences?: DocumentPreferences
|
||||
previousFormState: FormState
|
||||
renderAllFields: boolean
|
||||
renderFieldFn: RenderFieldMethod
|
||||
req: PayloadRequest
|
||||
/**
|
||||
* Whether to skip checking the field's condition. @default false
|
||||
*/
|
||||
skipConditionChecks?: boolean
|
||||
/**
|
||||
* Whether to skip validating the field. @default false
|
||||
*/
|
||||
skipValidation?: boolean
|
||||
state?: FormStateWithoutComponents
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens the fields schema and fields data
|
||||
*/
|
||||
export const iterateFields = async ({
|
||||
id,
|
||||
addErrorPathToParent: addErrorPathToParentArg,
|
||||
anyParentLocalized = false,
|
||||
collectionSlug,
|
||||
data,
|
||||
fields,
|
||||
fieldSchemaMap,
|
||||
filter,
|
||||
forceFullValue = false,
|
||||
fullData,
|
||||
includeSchema = false,
|
||||
omitParents = false,
|
||||
operation,
|
||||
parentIndexPath,
|
||||
parentPassesCondition = true,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
permissions,
|
||||
preferences,
|
||||
previousFormState,
|
||||
renderAllFields,
|
||||
renderFieldFn: renderFieldFn,
|
||||
req,
|
||||
skipConditionChecks = false,
|
||||
skipValidation = false,
|
||||
state = {},
|
||||
}: IterateFieldsArgs): Promise<void> => {
|
||||
const promises = []
|
||||
|
||||
fields.forEach((field, fieldIndex) => {
|
||||
let passesCondition = true
|
||||
if (!skipConditionChecks) {
|
||||
passesCondition = Boolean(
|
||||
(field?.admin?.condition
|
||||
? Boolean(field.admin.condition(fullData || {}, data || {}, { user: req.user }))
|
||||
: true) && parentPassesCondition,
|
||||
)
|
||||
}
|
||||
|
||||
promises.push(
|
||||
addFieldStatePromise({
|
||||
id,
|
||||
addErrorPathToParent: addErrorPathToParentArg,
|
||||
anyParentLocalized,
|
||||
collectionSlug,
|
||||
data,
|
||||
field,
|
||||
fieldIndex,
|
||||
fieldSchemaMap,
|
||||
filter,
|
||||
forceFullValue,
|
||||
fullData,
|
||||
includeSchema,
|
||||
omitParents,
|
||||
operation,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
passesCondition,
|
||||
permissions,
|
||||
preferences,
|
||||
previousFormState,
|
||||
renderAllFields,
|
||||
renderFieldFn,
|
||||
req,
|
||||
skipConditionChecks,
|
||||
skipValidation,
|
||||
state,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Data, Field as FieldSchema, User } from 'payload'
|
||||
|
||||
import { iterateFields } from './iterateFields.js'
|
||||
import { iterateFields } from './promise.js'
|
||||
|
||||
type Args = {
|
||||
data: Data
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { Data, Field, TabAsField, User } from 'payload'
|
||||
|
||||
import { defaultValuePromise } from './promise.js'
|
||||
|
||||
type Args<T> = {
|
||||
data: T
|
||||
fields: (Field | TabAsField)[]
|
||||
id?: number | string
|
||||
locale: string | undefined
|
||||
siblingData: Data
|
||||
user: User
|
||||
}
|
||||
|
||||
export const iterateFields = async <T>({
|
||||
id,
|
||||
data,
|
||||
fields,
|
||||
locale,
|
||||
siblingData,
|
||||
user,
|
||||
}: Args<T>): Promise<void> => {
|
||||
const promises = []
|
||||
|
||||
fields.forEach((field) => {
|
||||
promises.push(
|
||||
defaultValuePromise({
|
||||
id,
|
||||
data,
|
||||
field,
|
||||
locale,
|
||||
siblingData,
|
||||
user,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
@@ -3,8 +3,6 @@ import type { Data, Field, TabAsField, User } from 'payload'
|
||||
import { getDefaultValue } from 'payload'
|
||||
import { fieldAffectsData, tabHasName } from 'payload/shared'
|
||||
|
||||
import { iterateFields } from './iterateFields.js'
|
||||
|
||||
type Args<T> = {
|
||||
data: T
|
||||
field: Field | TabAsField
|
||||
@@ -168,3 +166,38 @@ export const defaultValuePromise = async <T>({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type IterateFieldsArgs<T> = {
|
||||
data: T
|
||||
fields: (Field | TabAsField)[]
|
||||
id?: number | string
|
||||
locale: string | undefined
|
||||
siblingData: Data
|
||||
user: User
|
||||
}
|
||||
|
||||
export const iterateFields = async <T>({
|
||||
id,
|
||||
data,
|
||||
fields,
|
||||
locale,
|
||||
siblingData,
|
||||
user,
|
||||
}: IterateFieldsArgs<T>): Promise<void> => {
|
||||
const promises = []
|
||||
|
||||
fields.forEach((field) => {
|
||||
promises.push(
|
||||
defaultValuePromise({
|
||||
id,
|
||||
data,
|
||||
field,
|
||||
locale,
|
||||
siblingData,
|
||||
user,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import type {
|
||||
|
||||
import type { RenderFieldMethod } from './types.js'
|
||||
|
||||
import { iterateFields } from './addFieldStatePromise.js'
|
||||
import { calculateDefaultValues } from './calculateDefaultValues/index.js'
|
||||
import { iterateFields } from './iterateFields.js'
|
||||
|
||||
type Args = {
|
||||
collectionSlug?: string
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import type {
|
||||
Data,
|
||||
DocumentPreferences,
|
||||
Field as FieldSchema,
|
||||
FieldSchemaMap,
|
||||
FormState,
|
||||
FormStateWithoutComponents,
|
||||
PayloadRequest,
|
||||
SanitizedFieldPermissions,
|
||||
} from 'payload'
|
||||
|
||||
import type { AddFieldStatePromiseArgs } from './addFieldStatePromise.js'
|
||||
import type { RenderFieldMethod } from './types.js'
|
||||
|
||||
import { addFieldStatePromise } from './addFieldStatePromise.js'
|
||||
|
||||
type Args = {
|
||||
addErrorPathToParent: (fieldPath: string) => void
|
||||
/**
|
||||
* if any parents is localized, then the field is localized. @default false
|
||||
*/
|
||||
anyParentLocalized?: boolean
|
||||
collectionSlug?: string
|
||||
data: Data
|
||||
fields: FieldSchema[]
|
||||
fieldSchemaMap: FieldSchemaMap
|
||||
filter?: (args: AddFieldStatePromiseArgs) => boolean
|
||||
/**
|
||||
* Force the value of fields like arrays or blocks to be the full value instead of the length @default false
|
||||
*/
|
||||
forceFullValue?: boolean
|
||||
fullData: Data
|
||||
id?: number | string
|
||||
/**
|
||||
* Whether the field schema should be included in the state. @default false
|
||||
*/
|
||||
includeSchema?: boolean
|
||||
/**
|
||||
* Whether to omit parent fields in the state. @default false
|
||||
*/
|
||||
omitParents?: boolean
|
||||
/**
|
||||
* operation is only needed for validation
|
||||
*/
|
||||
operation: 'create' | 'update'
|
||||
parentIndexPath: string
|
||||
parentPassesCondition?: boolean
|
||||
parentPath: string
|
||||
parentSchemaPath: string
|
||||
permissions:
|
||||
| {
|
||||
[fieldName: string]: SanitizedFieldPermissions
|
||||
}
|
||||
| null
|
||||
| SanitizedFieldPermissions
|
||||
preferences?: DocumentPreferences
|
||||
previousFormState: FormState
|
||||
renderAllFields: boolean
|
||||
renderFieldFn: RenderFieldMethod
|
||||
req: PayloadRequest
|
||||
/**
|
||||
* Whether to skip checking the field's condition. @default false
|
||||
*/
|
||||
skipConditionChecks?: boolean
|
||||
/**
|
||||
* Whether to skip validating the field. @default false
|
||||
*/
|
||||
skipValidation?: boolean
|
||||
state?: FormStateWithoutComponents
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens the fields schema and fields data
|
||||
*/
|
||||
export const iterateFields = async ({
|
||||
id,
|
||||
addErrorPathToParent: addErrorPathToParentArg,
|
||||
anyParentLocalized = false,
|
||||
collectionSlug,
|
||||
data,
|
||||
fields,
|
||||
fieldSchemaMap,
|
||||
filter,
|
||||
forceFullValue = false,
|
||||
fullData,
|
||||
includeSchema = false,
|
||||
omitParents = false,
|
||||
operation,
|
||||
parentIndexPath,
|
||||
parentPassesCondition = true,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
permissions,
|
||||
preferences,
|
||||
previousFormState,
|
||||
renderAllFields,
|
||||
renderFieldFn: renderFieldFn,
|
||||
req,
|
||||
skipConditionChecks = false,
|
||||
skipValidation = false,
|
||||
state = {},
|
||||
}: Args): Promise<void> => {
|
||||
const promises = []
|
||||
|
||||
fields.forEach((field, fieldIndex) => {
|
||||
let passesCondition = true
|
||||
if (!skipConditionChecks) {
|
||||
passesCondition = Boolean(
|
||||
(field?.admin?.condition
|
||||
? Boolean(field.admin.condition(fullData || {}, data || {}, { user: req.user }))
|
||||
: true) && parentPassesCondition,
|
||||
)
|
||||
}
|
||||
|
||||
promises.push(
|
||||
addFieldStatePromise({
|
||||
id,
|
||||
addErrorPathToParent: addErrorPathToParentArg,
|
||||
anyParentLocalized,
|
||||
collectionSlug,
|
||||
data,
|
||||
field,
|
||||
fieldIndex,
|
||||
fieldSchemaMap,
|
||||
filter,
|
||||
forceFullValue,
|
||||
fullData,
|
||||
includeSchema,
|
||||
omitParents,
|
||||
operation,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
passesCondition,
|
||||
permissions,
|
||||
preferences,
|
||||
previousFormState,
|
||||
renderAllFields,
|
||||
renderFieldFn,
|
||||
req,
|
||||
skipConditionChecks,
|
||||
skipValidation,
|
||||
state,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import type { PgTable } from 'drizzle-orm/pg-core'
|
||||
import type { SQLiteTable } from 'drizzle-orm/sqlite-core'
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import { GenericTable } from '@payloadcms/drizzle/types'
|
||||
import { sql } from 'drizzle-orm'
|
||||
|
||||
import { isMongoose } from './isMongoose.js'
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
],
|
||||
"paths": {
|
||||
"@payload-config": [
|
||||
"./test/_community/config.ts"
|
||||
"./test/fields/config.ts"
|
||||
],
|
||||
"@payloadcms/live-preview": [
|
||||
"./packages/live-preview/src"
|
||||
@@ -54,6 +54,42 @@
|
||||
"@payloadcms/ui/shared": [
|
||||
"./packages/ui/src/exports/shared/index.ts"
|
||||
],
|
||||
"@payloadcms/ui/forms/fieldSchemasToFormState": [
|
||||
"./packages/ui/src/forms/fieldSchemasToFormState/index.tsx"
|
||||
],
|
||||
"@payloadcms/ui/utilities/buildFieldSchemaMap/traverseFields": [
|
||||
"./packages/ui/src/utilities/buildFieldSchemaMap/traverseFields.ts"
|
||||
],
|
||||
"@payloadcms/translations/utilities": [
|
||||
"./packages/translations/src/exports/utilities.ts"
|
||||
],
|
||||
"@payloadcms/translations/languages/en": [
|
||||
"./packages/translations/src/languages/en.ts"
|
||||
],
|
||||
"@payloadcms/translations/languages/de": [
|
||||
"./packages/translations/src/languages/de.ts"
|
||||
],
|
||||
"@payloadcms/translations/languages/es": [
|
||||
"./packages/translations/src/languages/es.ts"
|
||||
],
|
||||
"payload": [
|
||||
"./packages/payload/src/index.ts"
|
||||
],
|
||||
"payload/shared": [
|
||||
"./packages/payload/src/exports/shared.ts"
|
||||
],
|
||||
"payload/i18n/en": [
|
||||
"./packages/payload/src/exports/i18n/en.ts"
|
||||
],
|
||||
"payload/i18n/de": [
|
||||
"./packages/payload/src/exports/i18n/de.ts"
|
||||
],
|
||||
"payload/i18n/es": [
|
||||
"./packages/payload/src/exports/i18n/es.ts"
|
||||
],
|
||||
"payload/node": [
|
||||
"./packages/payload/src/exports/node.ts"
|
||||
],
|
||||
"@payloadcms/ui/scss": [
|
||||
"./packages/ui/src/scss.scss"
|
||||
],
|
||||
@@ -69,6 +105,15 @@
|
||||
"@payloadcms/richtext-lexical/rsc": [
|
||||
"./packages/richtext-lexical/src/exports/server/rsc.ts"
|
||||
],
|
||||
"@payloadcms/richtext-lexical/migrate": [
|
||||
"./packages/richtext-lexical/src/exports/server/migrate.ts"
|
||||
],
|
||||
"@payloadcms/richtext-lexical/lexical/headless": [
|
||||
"./packages/richtext-lexical/src/lexical-proxy/@lexical-headless.ts"
|
||||
],
|
||||
"@payloadcms/richtext-lexical/lexical/markdown": [
|
||||
"./packages/richtext-lexical/src/lexical-proxy/@lexical-markdown.ts"
|
||||
],
|
||||
"@payloadcms/richtext-slate/rsc": [
|
||||
"./packages/richtext-slate/src/exports/server/rsc.ts"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user