Compare commits

...

24 Commits

Author SHA1 Message Date
Alessio Gravili
e5801528ae cleanup getEntityPolicies, optimize global doc lookup by adding missing depth: 0 2025-02-17 01:53:12 -07:00
Alessio Gravili
42c8f15161 adjust incorrect fieldWithinLocalizedField logic in buildProjectionFromSelect, rename withinLocalizedField to parentIsLocalized 2025-02-16 22:06:40 -07:00
Alessio Gravili
7d9fbafe5f make new arguments optional where they'd be a breaking change 2025-02-16 21:57:50 -07:00
Alessio Gravili
4e7266e927 introduce fieldShouldBeLocalized helper function 2025-02-16 21:03:17 -07:00
Alessio Gravili
7653386552 adjust test 2025-02-16 20:39:58 -07:00
Alessio Gravili
63eb41617e Merge remote-tracking branch 'origin/main' into fix/block-references-localization 2025-02-16 19:43:29 -07:00
Alessio Gravili
8c68ea9677 Merge remote-tracking branch 'origin/main' into fix/block-references-localization 2025-02-16 19:17:34 -07:00
Alessio Gravili
2a199d7724 handle parentIsLocalized for join field 2025-02-16 18:41:43 -07:00
Alessio Gravili
475fd5be92 address lint error 2025-02-16 14:15:35 -07:00
Alessio Gravili
35e9c27286 remover no longer needed int test 2025-02-16 14:12:55 -07:00
Alessio Gravili
618491d694 do not sanitize away field.localized in monorepo, to improve test coverage for runtime field localization handling 2025-02-16 01:55:40 -07:00
Alessio Gravili
2c8701b89d fix: incorrect leavesFirst implementation in traverseFields utility 2025-02-16 01:33:05 -07:00
Alessio Gravili
88f3ec562e fix join field localization in postgres 2025-02-16 01:09:51 -07:00
Alessio Gravili
869461b0ad fix int test data 2025-02-16 00:24:11 -07:00
Alessio Gravili
63e07ff481 fix int test 2025-02-16 00:20:47 -07:00
Alessio Gravili
603418fe83 work around pg bug 2025-02-16 00:18:02 -07:00
Alessio Gravili
ae2b3bb984 fix logic error 2025-02-16 00:17:46 -07:00
Alessio Gravili
6d9972ee8f chore: add PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY env variable for better testing 2025-02-15 23:53:28 -07:00
Alessio Gravili
a61431e9d8 fix build 2025-02-15 23:41:18 -07:00
Alessio Gravili
b17520dac6 prefer env variable over config compatibility property access 2025-02-15 23:39:12 -07:00
Alessio Gravili
cffe888e84 remove console.log in now-passing test 2025-02-15 23:30:50 -07:00
Alessio Gravili
505db12ff3 Merge remote-tracking branch 'origin/main' into fix/block-references-localization 2025-02-15 23:22:11 -07:00
Alessio Gravili
209b9ad291 ensure field localization is properly handled everywhere 2025-02-15 23:15:46 -07:00
Alessio Gravili
41353f2d94 chore: add failing int test 2025-02-13 18:54:48 -07:00
114 changed files with 2275 additions and 1540 deletions

View File

@@ -23,6 +23,7 @@ export const defaultESLintIgnores = [
'next-env.d.ts',
'**/app',
'src/**/*.spec.ts',
'**/jest.setup.js',
]
/** @typedef {import('eslint').Linter.Config} Config */

View File

@@ -27,6 +27,8 @@ const config = withBundleAnalyzer(
env: {
PAYLOAD_CORE_DEV: 'true',
ROOT_DIR: path.resolve(dirname),
// @todo remove in 4.0 - will behave like this by default in 4.0
PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY: 'true',
},
async redirects() {
return [

View File

@@ -45,6 +45,7 @@ export const find: Find = async function find(
config: this.payload.config,
fields: collectionConfig.flattenedFields,
locale,
parentIsLocalized: false,
sort: sortArg || collectionConfig.defaultSort,
timestamps: true,
})

View File

@@ -41,6 +41,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
config: this.payload.config,
fields: versionFields,
locale,
parentIsLocalized: false,
sort: sortArg || '-updatedAt',
timestamps: true,
})

View File

@@ -36,6 +36,7 @@ export const findVersions: FindVersions = async function findVersions(
config: this.payload.config,
fields: collectionConfig.flattenedFields,
locale,
parentIsLocalized: false,
sort: sortArg || '-updatedAt',
timestamps: true,
})

View File

@@ -26,15 +26,20 @@ export const init: Init = function init(this: MongooseAdapter) {
const versionCollectionFields = buildVersionCollectionFields(this.payload.config, collection)
const versionSchema = buildSchema(this.payload, versionCollectionFields, {
disableUnique: true,
draftsEnabled: true,
indexSortableFields: this.payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: false,
const versionSchema = buildSchema({
buildSchemaOptions: {
disableUnique: true,
draftsEnabled: true,
indexSortableFields: this.payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: false,
},
...schemaOptions,
},
...schemaOptions,
configFields: versionCollectionFields,
parentIsLocalized: false,
payload: this.payload,
})
versionSchema.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true }).plugin(
@@ -77,14 +82,19 @@ export const init: Init = function init(this: MongooseAdapter) {
const versionGlobalFields = buildVersionGlobalFields(this.payload.config, global)
const versionSchema = buildSchema(this.payload, versionGlobalFields, {
disableUnique: true,
draftsEnabled: true,
indexSortableFields: this.payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: false,
const versionSchema = buildSchema({
buildSchemaOptions: {
disableUnique: true,
draftsEnabled: true,
indexSortableFields: this.payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: false,
},
},
configFields: versionGlobalFields,
parentIsLocalized: false,
payload: this.payload,
})
versionSchema.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true }).plugin(

View File

@@ -12,14 +12,21 @@ export const buildCollectionSchema = (
payload: Payload,
schemaOptions = {},
): Schema => {
const schema = buildSchema(payload, collection.fields, {
draftsEnabled: Boolean(typeof collection?.versions === 'object' && collection.versions.drafts),
indexSortableFields: payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: collection.timestamps !== false,
...schemaOptions,
const schema = buildSchema({
buildSchemaOptions: {
draftsEnabled: Boolean(
typeof collection?.versions === 'object' && collection.versions.drafts,
),
indexSortableFields: payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: collection.timestamps !== false,
...schemaOptions,
},
},
configFields: collection.fields,
parentIsLocalized: false,
payload,
})
if (Array.isArray(collection.upload.filenameCompoundIndex)) {

View File

@@ -19,10 +19,15 @@ export const buildGlobalModel = (payload: Payload): GlobalModel | null => {
const Globals = mongoose.model('globals', globalsSchema, 'globals') as unknown as GlobalModel
Object.values(payload.config.globals).forEach((globalConfig) => {
const globalSchema = buildSchema(payload, globalConfig.fields, {
options: {
minimize: false,
const globalSchema = buildSchema({
buildSchemaOptions: {
options: {
minimize: false,
},
},
configFields: globalConfig.fields,
parentIsLocalized: false,
payload,
})
Globals.discriminator(globalConfig.slug, globalSchema)
})

View File

@@ -31,9 +31,9 @@ import {
} from 'payload'
import {
fieldAffectsData,
fieldIsLocalized,
fieldIsPresentationalOnly,
fieldIsVirtual,
fieldShouldBeLocalized,
tabHasName,
} from 'payload/shared'
@@ -50,6 +50,7 @@ type FieldSchemaGenerator = (
schema: Schema,
config: Payload,
buildSchemaOptions: BuildSchemaOptions,
parentIsLocalized: boolean,
) => void
/**
@@ -61,7 +62,15 @@ const formatDefaultValue = (field: FieldAffectingData) =>
? field.defaultValue
: undefined
const formatBaseSchema = (field: FieldAffectingData, buildSchemaOptions: BuildSchemaOptions) => {
const formatBaseSchema = ({
buildSchemaOptions,
field,
parentIsLocalized,
}: {
buildSchemaOptions: BuildSchemaOptions
field: FieldAffectingData
parentIsLocalized: boolean
}) => {
const { disableUnique, draftsEnabled, indexSortableFields } = buildSchemaOptions
const schema: SchemaTypeOptions<unknown> = {
default: formatDefaultValue(field),
@@ -72,7 +81,7 @@ const formatBaseSchema = (field: FieldAffectingData, buildSchemaOptions: BuildSc
if (
schema.unique &&
(field.localized ||
(fieldShouldBeLocalized({ field, parentIsLocalized }) ||
draftsEnabled ||
(fieldAffectsData(field) &&
field.type !== 'group' &&
@@ -93,8 +102,13 @@ const localizeSchema = (
entity: NonPresentationalField | Tab,
schema,
localization: false | SanitizedLocalizationConfig,
parentIsLocalized: boolean,
) => {
if (fieldIsLocalized(entity) && localization && Array.isArray(localization.locales)) {
if (
fieldShouldBeLocalized({ field: entity, parentIsLocalized }) &&
localization &&
Array.isArray(localization.locales)
) {
return {
type: localization.localeCodes.reduce(
(localeSchema, locale) => ({
@@ -111,11 +125,13 @@ const localizeSchema = (
return schema
}
export const buildSchema = (
payload: Payload,
configFields: Field[],
buildSchemaOptions: BuildSchemaOptions = {},
): Schema => {
export const buildSchema = (args: {
buildSchemaOptions: BuildSchemaOptions
configFields: Field[]
parentIsLocalized?: boolean
payload: Payload
}): Schema => {
const { buildSchemaOptions = {}, configFields, parentIsLocalized, payload } = args
const { allowIDField, options } = buildSchemaOptions
let fields = {}
@@ -144,7 +160,7 @@ export const buildSchema = (
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[field.type]
if (addFieldSchema) {
addFieldSchema(field, schema, payload, buildSchemaOptions)
addFieldSchema(field, schema, payload, buildSchemaOptions, parentIsLocalized)
}
}
})
@@ -153,44 +169,49 @@ export const buildSchema = (
}
const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
array: (
field: ArrayField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
) => {
array: (field: ArrayField, schema, payload, buildSchemaOptions, parentIsLocalized) => {
const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: [
buildSchema(payload, field.fields, {
allowIDField: true,
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
options: {
_id: false,
id: false,
minimize: false,
buildSchema({
buildSchemaOptions: {
allowIDField: true,
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
options: {
_id: false,
id: false,
minimize: false,
},
},
configFields: field.fields,
parentIsLocalized: parentIsLocalized || field.localized,
payload,
}),
],
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
},
blocks: (
field: BlocksField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
blocks: (field: BlocksField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
const fieldSchema = {
type: [new mongoose.Schema({}, { _id: false, discriminatorKey: 'blockType' })],
}
schema.add({
[field.name]: localizeSchema(field, fieldSchema, payload.config.localization),
[field.name]: localizeSchema(
field,
fieldSchema,
payload.config.localization,
parentIsLocalized,
),
})
;(field.blockReferences ?? field.blocks).forEach((blockItem) => {
const blockSchema = new mongoose.Schema({}, { _id: false, id: false })
@@ -200,11 +221,17 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
block.fields.forEach((blockField) => {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[blockField.type]
if (addFieldSchema) {
addFieldSchema(blockField, blockSchema, payload, buildSchemaOptions)
addFieldSchema(
blockField,
blockSchema,
payload,
buildSchemaOptions,
parentIsLocalized || field.localized,
)
}
})
if (field.localized && payload.config.localization) {
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) {
payload.config.localization.localeCodes.forEach((localeCode) => {
// @ts-expect-error Possible incorrect typing in mongoose types, this works
schema.path(`${field.name}.${localeCode}`).discriminator(block.slug, blockSchema)
@@ -217,33 +244,46 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
},
checkbox: (
field: CheckboxField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
schema,
payload,
buildSchemaOptions,
parentIsLocalized,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Boolean }
const baseSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: Boolean,
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
},
code: (
field: CodeField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
code: (field: CodeField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
const baseSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: String,
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
},
collapsible: (
field: CollapsibleField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
schema,
payload,
buildSchemaOptions,
parentIsLocalized,
): void => {
field.fields.forEach((subField: Field) => {
if (fieldIsVirtual(subField)) {
@@ -253,41 +293,42 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
if (addFieldSchema) {
addFieldSchema(subField, schema, payload, buildSchemaOptions)
addFieldSchema(subField, schema, payload, buildSchemaOptions, parentIsLocalized)
}
})
},
date: (
field: DateField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Date }
date: (field: DateField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
const baseSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: Date,
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
},
email: (
field: EmailField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
email: (field: EmailField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
const baseSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: String,
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
},
group: (
field: GroupField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
const formattedBaseSchema = formatBaseSchema(field, buildSchemaOptions)
group: (field: GroupField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
const formattedBaseSchema = formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized })
// carry indexSortableFields through to versions if drafts enabled
const indexSortableFields =
@@ -297,58 +338,63 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
const baseSchema = {
...formattedBaseSchema,
type: buildSchema(payload, field.fields, {
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
indexSortableFields,
options: {
_id: false,
id: false,
minimize: false,
type: buildSchema({
buildSchemaOptions: {
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
indexSortableFields,
options: {
_id: false,
id: false,
minimize: false,
},
},
configFields: field.fields,
parentIsLocalized: parentIsLocalized || field.localized,
payload,
}),
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
},
json: (
field: JSONField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
json: (field: JSONField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: mongoose.Schema.Types.Mixed,
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
},
number: (
field: NumberField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
number: (field: NumberField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: field.hasMany ? [Number] : Number,
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
},
point: (
field: PointField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
point: (field: PointField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
const baseSchema: SchemaTypeOptions<unknown> = {
type: {
type: String,
@@ -363,12 +409,21 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
required: false,
},
}
if (buildSchemaOptions.disableUnique && field.unique && field.localized) {
if (
buildSchemaOptions.disableUnique &&
field.unique &&
fieldShouldBeLocalized({ field, parentIsLocalized })
) {
baseSchema.coordinates.sparse = true
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
if (field.index === true || field.index === undefined) {
@@ -377,7 +432,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
indexOptions.sparse = true
indexOptions.unique = true
}
if (field.localized && payload.config.localization) {
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) {
payload.config.localization.locales.forEach((locale) => {
schema.index({ [`${field.name}.${locale.code}`]: '2dsphere' }, indexOptions)
})
@@ -386,14 +441,9 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
}
},
radio: (
field: RadioField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
radio: (field: RadioField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: String,
enum: field.options.map((option) => {
if (typeof option === 'object') {
@@ -404,28 +454,34 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
},
relationship: (
field: RelationshipField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
schema,
payload,
buildSchemaOptions,
parentIsLocalized,
) => {
const hasManyRelations = Array.isArray(field.relationTo)
let schemaToReturn: { [key: string]: any } = {}
const valueType = getRelationshipValueType(field, payload)
if (field.localized && payload.config.localization) {
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) {
schemaToReturn = {
type: payload.config.localization.localeCodes.reduce((locales, locale) => {
let localeSchema: { [key: string]: any } = {}
if (hasManyRelations) {
localeSchema = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
_id: false,
type: mongoose.Schema.Types.Mixed,
relationTo: { type: String, enum: field.relationTo },
@@ -436,7 +492,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
} else {
localeSchema = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: valueType,
ref: field.relationTo,
}
@@ -453,7 +509,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
} else if (hasManyRelations) {
schemaToReturn = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
_id: false,
type: mongoose.Schema.Types.Mixed,
relationTo: { type: String, enum: field.relationTo },
@@ -471,7 +527,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
} else {
schemaToReturn = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: valueType,
ref: field.relationTo,
}
@@ -490,25 +546,26 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
},
richText: (
field: RichTextField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
schema,
payload,
buildSchemaOptions,
parentIsLocalized,
): void => {
const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: mongoose.Schema.Types.Mixed,
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
},
row: (
field: RowField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
row: (field: RowField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
field.fields.forEach((subField: Field) => {
if (fieldIsVirtual(subField)) {
return
@@ -517,18 +574,13 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
if (addFieldSchema) {
addFieldSchema(subField, schema, payload, buildSchemaOptions)
addFieldSchema(subField, schema, payload, buildSchemaOptions, parentIsLocalized)
}
})
},
select: (
field: SelectField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
select: (field: SelectField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: String,
enum: field.options.map((option) => {
if (typeof option === 'object') {
@@ -547,34 +599,40 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
field,
field.hasMany ? [baseSchema] : baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
},
tabs: (
field: TabsField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
tabs: (field: TabsField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
field.tabs.forEach((tab) => {
if (tabHasName(tab)) {
if (fieldIsVirtual(tab)) {
return
}
const baseSchema = {
type: buildSchema(payload, tab.fields, {
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
options: {
_id: false,
id: false,
minimize: false,
type: buildSchema({
buildSchemaOptions: {
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
options: {
_id: false,
id: false,
minimize: false,
},
},
configFields: tab.fields,
parentIsLocalized: parentIsLocalized || tab.localized,
payload,
}),
}
schema.add({
[tab.name]: localizeSchema(tab, baseSchema, payload.config.localization),
[tab.name]: localizeSchema(
tab,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
} else {
tab.fields.forEach((subField: Field) => {
@@ -584,58 +642,68 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
if (addFieldSchema) {
addFieldSchema(subField, schema, payload, buildSchemaOptions)
addFieldSchema(
subField,
schema,
payload,
buildSchemaOptions,
parentIsLocalized || tab.localized,
)
}
})
}
})
},
text: (
field: TextField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
text: (field: TextField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: field.hasMany ? [String] : String,
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
},
textarea: (
field: TextareaField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
schema,
payload,
buildSchemaOptions,
parentIsLocalized,
): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
const baseSchema = {
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: String,
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization),
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
},
upload: (
field: UploadField,
schema: Schema,
payload: Payload,
buildSchemaOptions: BuildSchemaOptions,
): void => {
upload: (field: UploadField, schema, payload, buildSchemaOptions, parentIsLocalized): void => {
const hasManyRelations = Array.isArray(field.relationTo)
let schemaToReturn: { [key: string]: any } = {}
const valueType = getRelationshipValueType(field, payload)
if (field.localized && payload.config.localization) {
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) {
schemaToReturn = {
type: payload.config.localization.localeCodes.reduce((locales, locale) => {
let localeSchema: { [key: string]: any } = {}
if (hasManyRelations) {
localeSchema = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
_id: false,
type: mongoose.Schema.Types.Mixed,
relationTo: { type: String, enum: field.relationTo },
@@ -646,7 +714,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
} else {
localeSchema = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: valueType,
ref: field.relationTo,
}
@@ -663,7 +731,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
} else if (hasManyRelations) {
schemaToReturn = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
_id: false,
type: mongoose.Schema.Types.Mixed,
relationTo: { type: String, enum: field.relationTo },
@@ -681,7 +749,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
} else {
schemaToReturn = {
...formatBaseSchema(field, buildSchemaOptions),
...formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized }),
type: valueType,
ref: field.relationTo,
}

View File

@@ -13,12 +13,14 @@ const migrateModelWithBatching = async ({
config,
fields,
Model,
parentIsLocalized,
session,
}: {
batchSize: number
config: SanitizedConfig
fields: Field[]
Model: Model<any>
parentIsLocalized: boolean
session: ClientSession
}): Promise<void> => {
let hasNext = true
@@ -47,7 +49,7 @@ const migrateModelWithBatching = async ({
}
for (const doc of docs) {
sanitizeRelationshipIDs({ config, data: doc, fields })
sanitizeRelationshipIDs({ config, data: doc, fields, parentIsLocalized })
}
await Model.collection.bulkWrite(
@@ -124,6 +126,7 @@ export async function migrateRelationshipsV2_V3({
config,
fields: collection.fields,
Model: db.collections[collection.slug],
parentIsLocalized: false,
session,
})
@@ -138,6 +141,7 @@ export async function migrateRelationshipsV2_V3({
config,
fields: buildVersionCollectionFields(config, collection),
Model: db.versions[collection.slug],
parentIsLocalized: false,
session,
})
@@ -163,7 +167,11 @@ export async function migrateRelationshipsV2_V3({
// in case if the global doesn't exist in the database yet (not saved)
if (doc) {
sanitizeRelationshipIDs({ config, data: doc, fields: global.fields })
sanitizeRelationshipIDs({
config,
data: doc,
fields: global.fields,
})
await GlobalsModel.collection.updateOne(
{
@@ -185,6 +193,7 @@ export async function migrateRelationshipsV2_V3({
config,
fields: buildVersionGlobalFields(config, global),
Model: db.versions[global.slug],
parentIsLocalized: false,
session,
})

View File

@@ -7,6 +7,7 @@ export async function buildAndOrConditions({
fields,
globalSlug,
locale,
parentIsLocalized,
payload,
where,
}: {
@@ -14,6 +15,7 @@ export async function buildAndOrConditions({
fields: FlattenedField[]
globalSlug?: string
locale?: string
parentIsLocalized: boolean
payload: Payload
where: Where[]
}): Promise<Record<string, unknown>[]> {
@@ -29,6 +31,7 @@ export async function buildAndOrConditions({
fields,
globalSlug,
locale,
parentIsLocalized,
payload,
where: condition,
})

View File

@@ -47,6 +47,7 @@ export const getBuildQueryPlugin = ({
fields,
globalSlug,
locale,
parentIsLocalized: false,
payload,
where,
})

View File

@@ -30,6 +30,7 @@ export async function buildSearchParam({
incomingPath,
locale,
operator,
parentIsLocalized,
payload,
val,
}: {
@@ -39,6 +40,7 @@ export async function buildSearchParam({
incomingPath: string
locale?: string
operator: string
parentIsLocalized: boolean
payload: Payload
val: unknown
}): Promise<SearchParam> {
@@ -69,6 +71,7 @@ export async function buildSearchParam({
name: 'id',
type: idFieldType,
} as FlattenedField,
parentIsLocalized,
path: '_id',
})
} else {
@@ -78,6 +81,7 @@ export async function buildSearchParam({
globalSlug,
incomingPath: sanitizedPath,
locale,
parentIsLocalized,
payload,
})
}
@@ -89,6 +93,7 @@ export async function buildSearchParam({
hasCustomID,
locale,
operator,
parentIsLocalized,
path,
payload,
val,

View File

@@ -7,6 +7,7 @@ type Args = {
config: SanitizedConfig
fields: FlattenedField[]
locale: string
parentIsLocalized: boolean
sort: Sort
timestamps: boolean
}
@@ -22,6 +23,7 @@ export const buildSortParam = ({
config,
fields,
locale,
parentIsLocalized,
sort,
timestamps,
}: Args): PaginateOptions['sort'] => {
@@ -55,6 +57,7 @@ export const buildSortParam = ({
config,
fields,
locale,
parentIsLocalized,
segments: sortProperty.split('.'),
})
acc[localizedProperty] = sortDirection

View File

@@ -1,11 +1,12 @@
import type { FlattenedField, SanitizedConfig } from 'payload'
import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared'
import { fieldAffectsData, fieldIsPresentationalOnly, fieldShouldBeLocalized } from 'payload/shared'
type Args = {
config: SanitizedConfig
fields: FlattenedField[]
locale: string
parentIsLocalized: boolean
result?: string
segments: string[]
}
@@ -14,6 +15,7 @@ export const getLocalizedSortProperty = ({
config,
fields,
locale,
parentIsLocalized,
result: incomingResult,
segments: incomingSegments,
}: Args): string => {
@@ -35,10 +37,11 @@ export const getLocalizedSortProperty = ({
if (matchedField && !fieldIsPresentationalOnly(matchedField)) {
let nextFields: FlattenedField[]
let nextParentIsLocalized = parentIsLocalized
const remainingSegments = [...segments]
let localizedSegment = matchedField.name
if (matchedField.localized) {
if (fieldShouldBeLocalized({ field: matchedField, parentIsLocalized })) {
// Check to see if next segment is a locale
if (segments.length > 0) {
const nextSegmentIsLocale = config.localization.localeCodes.includes(remainingSegments[0])
@@ -62,6 +65,9 @@ export const getLocalizedSortProperty = ({
matchedField.type === 'array'
) {
nextFields = matchedField.flattenedFields
if (!nextParentIsLocalized) {
nextParentIsLocalized = matchedField.localized
}
}
if (matchedField.type === 'blocks') {
@@ -92,6 +98,7 @@ export const getLocalizedSortProperty = ({
config,
fields: nextFields,
locale,
parentIsLocalized: nextParentIsLocalized,
result,
segments: remainingSegments,
})

View File

@@ -12,6 +12,7 @@ export async function parseParams({
fields,
globalSlug,
locale,
parentIsLocalized,
payload,
where,
}: {
@@ -19,6 +20,7 @@ export async function parseParams({
fields: FlattenedField[]
globalSlug?: string
locale: string
parentIsLocalized: boolean
payload: Payload
where: Where
}): Promise<Record<string, unknown>> {
@@ -40,6 +42,7 @@ export async function parseParams({
fields,
globalSlug,
locale,
parentIsLocalized,
payload,
where: condition,
})
@@ -63,6 +66,7 @@ export async function parseParams({
incomingPath: relationOrPath,
locale,
operator,
parentIsLocalized,
payload,
val: pathOperators[operator],
})

View File

@@ -8,12 +8,14 @@ import type {
import { Types } from 'mongoose'
import { createArrayFromCommaDelineated } from 'payload'
import { fieldShouldBeLocalized } from 'payload/shared'
type SanitizeQueryValueArgs = {
field: FlattenedField
hasCustomID: boolean
locale?: string
operator: string
parentIsLocalized: boolean
path: string
payload: Payload
val: any
@@ -87,6 +89,7 @@ export const sanitizeQueryValue = ({
hasCustomID,
locale,
operator,
parentIsLocalized,
path,
payload,
val,
@@ -219,7 +222,11 @@ export const sanitizeQueryValue = ({
let localizedPath = path
if (field.localized && payload.config.localization && locale) {
if (
fieldShouldBeLocalized({ field, parentIsLocalized }) &&
payload.config.localization &&
locale
) {
localizedPath = `${path}.${locale}`
}

View File

@@ -34,6 +34,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
config: this.payload.config,
fields: collectionConfig.flattenedFields,
locale,
parentIsLocalized: false,
sort: sortArg || collectionConfig.defaultSort,
timestamps: true,
})

View File

@@ -1,6 +1,8 @@
import type { PipelineStage } from 'mongoose'
import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload'
import { fieldShouldBeLocalized } from 'payload/shared'
import type { MongooseAdapter } from '../index.js'
import { buildSortParam } from '../queries/buildSortParam.js'
@@ -76,6 +78,7 @@ export const buildJoinAggregation = async ({
config: adapter.payload.config,
fields: adapter.payload.collections[slug].config.flattenedFields,
locale,
parentIsLocalized: false,
sort: sortJoin,
timestamps: true,
})
@@ -148,7 +151,14 @@ export const buildJoinAggregation = async ({
})
} else {
const localeSuffix =
join.field.localized && adapter.payload.config.localization && locale ? `.${locale}` : ''
fieldShouldBeLocalized({
field: join.field,
parentIsLocalized: join.parentIsLocalized,
}) &&
adapter.payload.config.localization &&
locale
? `.${locale}`
: ''
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${localeSuffix}`
let foreignField: string

View File

@@ -1,6 +1,11 @@
import type { FieldAffectingData, FlattenedField, SelectMode, SelectType } from 'payload'
import { deepCopyObjectSimple, fieldAffectsData, getSelectMode } from 'payload/shared'
import {
deepCopyObjectSimple,
fieldAffectsData,
fieldShouldBeLocalized,
getSelectMode,
} from 'payload/shared'
import type { MongooseAdapter } from '../index.js'
@@ -8,18 +13,18 @@ const addFieldToProjection = ({
adapter,
databaseSchemaPath,
field,
parentIsLocalized,
projection,
withinLocalizedField,
}: {
adapter: MongooseAdapter
databaseSchemaPath: string
field: FieldAffectingData
parentIsLocalized: boolean
projection: Record<string, true>
withinLocalizedField: boolean
}) => {
const { config } = adapter.payload
if (withinLocalizedField && config.localization) {
if (parentIsLocalized && config.localization) {
for (const locale of config.localization.localeCodes) {
const localeDatabaseSchemaPath = databaseSchemaPath.replace('<locale>', locale)
projection[`${localeDatabaseSchemaPath}${field.name}`] = true
@@ -33,20 +38,20 @@ const traverseFields = ({
adapter,
databaseSchemaPath = '',
fields,
parentIsLocalized = false,
projection,
select,
selectAllOnCurrentLevel = false,
selectMode,
withinLocalizedField = false,
}: {
adapter: MongooseAdapter
databaseSchemaPath?: string
fields: FlattenedField[]
parentIsLocalized?: boolean
projection: Record<string, true>
select: SelectType
selectAllOnCurrentLevel?: boolean
selectMode: SelectMode
withinLocalizedField?: boolean
}) => {
for (const field of fields) {
if (fieldAffectsData(field)) {
@@ -56,8 +61,8 @@ const traverseFields = ({
adapter,
databaseSchemaPath,
field,
parentIsLocalized,
projection,
withinLocalizedField,
})
continue
}
@@ -73,8 +78,8 @@ const traverseFields = ({
adapter,
databaseSchemaPath,
field,
parentIsLocalized,
projection,
withinLocalizedField,
})
continue
}
@@ -86,14 +91,12 @@ const traverseFields = ({
}
let fieldDatabaseSchemaPath = databaseSchemaPath
let fieldWithinLocalizedField = withinLocalizedField
if (fieldAffectsData(field)) {
fieldDatabaseSchemaPath = `${databaseSchemaPath}${field.name}.`
if (field.localized) {
if (fieldShouldBeLocalized({ field, parentIsLocalized })) {
fieldDatabaseSchemaPath = `${fieldDatabaseSchemaPath}<locale>.`
fieldWithinLocalizedField = true
}
}
@@ -111,10 +114,10 @@ const traverseFields = ({
adapter,
databaseSchemaPath: fieldDatabaseSchemaPath,
fields: field.flattenedFields,
parentIsLocalized: parentIsLocalized || field.localized,
projection,
select: fieldSelect,
selectMode,
withinLocalizedField: fieldWithinLocalizedField,
})
break
@@ -133,11 +136,11 @@ const traverseFields = ({
adapter,
databaseSchemaPath: fieldDatabaseSchemaPath,
fields: block.flattenedFields,
parentIsLocalized: parentIsLocalized || field.localized,
projection,
select: {},
selectAllOnCurrentLevel: true,
selectMode: 'include',
withinLocalizedField: fieldWithinLocalizedField,
})
continue
}
@@ -161,10 +164,10 @@ const traverseFields = ({
adapter,
databaseSchemaPath: fieldDatabaseSchemaPath,
fields: block.flattenedFields,
parentIsLocalized: parentIsLocalized || field.localized,
projection,
select: blocksSelect[block.slug] as SelectType,
selectMode: blockSelectMode,
withinLocalizedField: fieldWithinLocalizedField,
})
}

View File

@@ -2,12 +2,13 @@ import type { CollectionConfig, Field, SanitizedConfig, TraverseFieldsCallback }
import { Types } from 'mongoose'
import { traverseFields } from 'payload'
import { fieldAffectsData } from 'payload/shared'
import { fieldAffectsData, fieldShouldBeLocalized } from 'payload/shared'
type Args = {
config: SanitizedConfig
data: Record<string, unknown>
fields: Field[]
parentIsLocalized?: boolean
}
interface RelationObject {
@@ -112,6 +113,7 @@ export const sanitizeRelationshipIDs = ({
config,
data,
fields,
parentIsLocalized,
}: Args): Record<string, unknown> => {
const sanitize: TraverseFieldsCallback = ({ field, ref }) => {
if (!ref || typeof ref !== 'object') {
@@ -124,7 +126,7 @@ export const sanitizeRelationshipIDs = ({
}
// handle localized relationships
if (config.localization && field.localized) {
if (config.localization && fieldShouldBeLocalized({ field, parentIsLocalized })) {
const locales = config.localization.locales
const fieldRef = ref[field.name]
if (typeof fieldRef !== 'object') {
@@ -150,7 +152,14 @@ export const sanitizeRelationshipIDs = ({
}
}
traverseFields({ callback: sanitize, config, fields, fillEmpty: false, ref: data })
traverseFields({
callback: sanitize,
config,
fields,
fillEmpty: false,
parentIsLocalized,
ref: data,
})
return data
}

View File

@@ -65,6 +65,7 @@ export const deleteOne: DeleteOne = async function deleteOne(
data: docToDelete,
fields: collection.flattenedFields,
joinQuery: false,
parentIsLocalized: false,
})
await this.deleteWhere({

View File

@@ -165,6 +165,7 @@ export const findMany = async function find({
data,
fields,
joinQuery,
parentIsLocalized: false,
})
})

View File

@@ -2,7 +2,7 @@ import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type { FlattenedField, JoinQuery, SelectMode, SelectType, Where } from 'payload'
import { sql } from 'drizzle-orm'
import { fieldIsVirtual } from 'payload/shared'
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type { BuildQueryJoinAliases, ChainedMethods, DrizzleAdapter } from '../types.js'
@@ -26,6 +26,7 @@ type TraverseFieldArgs = {
joinQuery: JoinQuery
joins?: BuildQueryJoinAliases
locale?: string
parentIsLocalized?: boolean
path: string
select?: SelectType
selectAllOnCurrentLevel?: boolean
@@ -34,7 +35,6 @@ type TraverseFieldArgs = {
topLevelArgs: Record<string, unknown>
topLevelTableName: string
versions?: boolean
withinLocalizedField?: boolean
withTabledFields: {
numbers?: boolean
rels?: boolean
@@ -53,6 +53,7 @@ export const traverseFields = ({
joinQuery = {},
joins,
locale,
parentIsLocalized = false,
path,
select,
selectAllOnCurrentLevel = false,
@@ -61,7 +62,6 @@ export const traverseFields = ({
topLevelArgs,
topLevelTableName,
versions,
withinLocalizedField = false,
withTabledFields,
}: TraverseFieldArgs) => {
fields.forEach((field) => {
@@ -69,6 +69,11 @@ export const traverseFields = ({
return
}
const isFieldLocalized = fieldShouldBeLocalized({
field,
parentIsLocalized,
})
// handle simple relationship
if (
depth > 0 &&
@@ -76,7 +81,7 @@ export const traverseFields = ({
!field.hasMany &&
typeof field.relationTo === 'string'
) {
if (field.localized) {
if (isFieldLocalized) {
_locales.with[`${path}${field.name}`] = true
} else {
currentArgs.with[`${path}${field.name}`] = true
@@ -152,13 +157,13 @@ export const traverseFields = ({
fields: field.flattenedFields,
joinQuery,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
path: '',
select: typeof arraySelect === 'object' ? arraySelect : undefined,
selectMode,
tablePath: '',
topLevelArgs,
topLevelTableName,
withinLocalizedField: withinLocalizedField || field.localized,
withTabledFields,
})
@@ -263,13 +268,13 @@ export const traverseFields = ({
fields: block.flattenedFields,
joinQuery,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
path: '',
select: typeof blockSelect === 'object' ? blockSelect : undefined,
selectMode: blockSelectMode,
tablePath: '',
topLevelArgs,
topLevelTableName,
withinLocalizedField: withinLocalizedField || field.localized,
withTabledFields,
})
@@ -305,6 +310,7 @@ export const traverseFields = ({
joinQuery,
joins,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${path}${field.name}_`,
select: typeof fieldSelect === 'object' ? fieldSelect : undefined,
selectAllOnCurrentLevel:
@@ -316,7 +322,6 @@ export const traverseFields = ({
topLevelArgs,
topLevelTableName,
versions,
withinLocalizedField: withinLocalizedField || field.localized,
withTabledFields,
})
@@ -407,6 +412,9 @@ export const traverseFields = ({
fields,
joins,
locale,
// Parent is never localized, as we're passing the `fields` of a **different** collection here. This means that the
// parent localization "boundary" is crossed, and we're now in the context of the joined collection.
parentIsLocalized: false,
selectLocale: true,
sort,
tableName: joinCollectionTableName,
@@ -469,7 +477,7 @@ export const traverseFields = ({
break
}
const args = field.localized ? _locales : currentArgs
const args = isFieldLocalized ? _locales : currentArgs
if (!args.columns) {
args.columns = {}
}
@@ -531,7 +539,7 @@ export const traverseFields = ({
if (select || selectAllOnCurrentLevel) {
const fieldPath = `${path}${field.name}`
if ((field.localized || withinLocalizedField) && _locales) {
if ((isFieldLocalized || parentIsLocalized) && _locales) {
_locales.columns[fieldPath] = true
} else if (adapter.tables[currentTableName]?.[fieldPath]) {
currentArgs.columns[fieldPath] = true
@@ -553,7 +561,7 @@ export const traverseFields = ({
) {
const fieldPath = `${path}${field.name}`
if ((field.localized || withinLocalizedField) && _locales) {
if ((isFieldLocalized || parentIsLocalized) && _locales) {
_locales.columns[fieldPath] = true
} else if (adapter.tables[currentTableName]?.[fieldPath]) {
currentArgs.columns[fieldPath] = true

View File

@@ -12,6 +12,7 @@ export function buildAndOrConditions({
fields,
joins,
locale,
parentIsLocalized,
selectFields,
selectLocale,
tableName,
@@ -24,6 +25,7 @@ export function buildAndOrConditions({
globalSlug?: string
joins: BuildQueryJoinAliases
locale?: string
parentIsLocalized: boolean
selectFields: Record<string, GenericColumn>
selectLocale?: boolean
tableName: string
@@ -42,6 +44,7 @@ export function buildAndOrConditions({
fields,
joins,
locale,
parentIsLocalized,
selectFields,
selectLocale,
tableName,

View File

@@ -15,6 +15,7 @@ type Args = {
fields: FlattenedField[]
joins: BuildQueryJoinAliases
locale?: string
parentIsLocalized: boolean
selectFields: Record<string, GenericColumn>
sort?: Sort
tableName: string
@@ -29,6 +30,7 @@ export const buildOrderBy = ({
fields,
joins,
locale,
parentIsLocalized,
selectFields,
sort,
tableName,
@@ -65,6 +67,7 @@ export const buildOrderBy = ({
fields,
joins,
locale,
parentIsLocalized,
pathSegments: sortProperty.replace(/__/g, '.').split('.'),
selectFields,
tableName,

View File

@@ -20,6 +20,7 @@ type BuildQueryArgs = {
fields: FlattenedField[]
joins?: BuildQueryJoinAliases
locale?: string
parentIsLocalized?: boolean
selectLocale?: boolean
sort?: Sort
tableName: string
@@ -41,6 +42,7 @@ const buildQuery = function buildQuery({
fields,
joins = [],
locale,
parentIsLocalized,
selectLocale,
sort,
tableName,
@@ -56,6 +58,7 @@ const buildQuery = function buildQuery({
fields,
joins,
locale,
parentIsLocalized,
selectFields,
sort,
tableName,
@@ -70,6 +73,7 @@ const buildQuery = function buildQuery({
fields,
joins,
locale,
parentIsLocalized,
selectFields,
selectLocale,
tableName,

View File

@@ -5,7 +5,7 @@ import type { FlattenedBlock, FlattenedField, NumberField, TextField } from 'pay
import { and, eq, like, sql } from 'drizzle-orm'
import { type PgTableWithColumns } from 'drizzle-orm/pg-core'
import { APIError } from 'payload'
import { tabHasName } from 'payload/shared'
import { fieldShouldBeLocalized, tabHasName } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import { validate as uuidValidate } from 'uuid'
@@ -46,6 +46,7 @@ type Args = {
fields: FlattenedField[]
joins: BuildQueryJoinAliases
locale?: string
parentIsLocalized: boolean
pathSegments: string[]
rootTableName?: string
selectFields: Record<string, GenericColumn>
@@ -75,6 +76,7 @@ export const getTableColumnFromPath = ({
fields,
joins,
locale: incomingLocale,
parentIsLocalized,
pathSegments: incomingSegments,
rootTableName: incomingRootTableName,
selectFields,
@@ -107,9 +109,11 @@ export const getTableColumnFromPath = ({
if (field) {
const pathSegments = [...incomingSegments]
const isFieldLocalized = fieldShouldBeLocalized({ field, parentIsLocalized })
// If next segment is a locale,
// we need to take it out and use it as the locale from this point on
if ('localized' in field && field.localized && adapter.payload.config.localization) {
if (isFieldLocalized && adapter.payload.config.localization) {
const matchedLocale = adapter.payload.config.localization.localeCodes.find(
(locale) => locale === pathSegments[1],
)
@@ -129,7 +133,7 @@ export const getTableColumnFromPath = ({
const arrayParentTable = aliasTable || adapter.tables[tableName]
constraintPath = `${constraintPath}${field.name}.%.`
if (locale && field.localized && adapter.payload.config.localization) {
if (locale && isFieldLocalized && adapter.payload.config.localization) {
const conditions = [eq(arrayParentTable.id, adapter.tables[newTableName]._parentID)]
if (selectLocale) {
@@ -159,6 +163,7 @@ export const getTableColumnFromPath = ({
fields: field.flattenedFields,
joins,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
@@ -224,6 +229,7 @@ export const getTableColumnFromPath = ({
fields: block.flattenedFields,
joins,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields: blockSelectFields,
@@ -240,7 +246,7 @@ export const getTableColumnFromPath = ({
blockTableColumn = result
constraints = constraints.concat(blockConstraints)
selectFields = { ...selectFields, ...blockSelectFields }
if (field.localized && adapter.payload.config.localization) {
if (isFieldLocalized && adapter.payload.config.localization) {
const conditions = [
eq(
(aliasTable || adapter.tables[tableName]).id,
@@ -281,7 +287,7 @@ export const getTableColumnFromPath = ({
}
case 'group': {
if (locale && field.localized && adapter.payload.config.localization) {
if (locale && isFieldLocalized && adapter.payload.config.localization) {
newTableName = `${tableName}${adapter.localesSuffix}`
let condition = eq(adapter.tables[tableName].id, adapter.tables[newTableName]._parentID)
@@ -306,6 +312,7 @@ export const getTableColumnFromPath = ({
fields: field.flattenedFields,
joins,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
@@ -331,7 +338,7 @@ export const getTableColumnFromPath = ({
like(adapter.tables[newTableName].path, `${constraintPath}${field.name}`),
]
if (locale && field.localized && adapter.payload.config.localization) {
if (locale && isFieldLocalized && adapter.payload.config.localization) {
const conditions = [...joinConstraints]
if (locale !== 'all') {
@@ -375,12 +382,12 @@ export const getTableColumnFromPath = ({
tableName: relationTableName,
})
if (selectLocale && field.localized && adapter.payload.config.localization) {
if (selectLocale && isFieldLocalized && adapter.payload.config.localization) {
selectFields._locale = aliasRelationshipTable.locale
}
// Join in the relationships table
if (locale && field.localized && adapter.payload.config.localization) {
if (locale && isFieldLocalized && adapter.payload.config.localization) {
const conditions = [
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
@@ -546,9 +553,11 @@ export const getTableColumnFromPath = ({
aliasTable: newAliasTable,
collectionPath: newCollectionPath,
constraints,
// relationshipFields are fields from a different collection => no parentIsLocalized
fields: relationshipFields,
joins,
locale,
parentIsLocalized: false,
pathSegments: pathSegments.slice(1),
rootTableName: newTableName,
selectFields,
@@ -567,7 +576,7 @@ export const getTableColumnFromPath = ({
)
const { newAliasTable } = getTableAlias({ adapter, tableName: newTableName })
if (field.localized && adapter.payload.config.localization) {
if (isFieldLocalized && adapter.payload.config.localization) {
const { newAliasTable: aliasLocaleTable } = getTableAlias({
adapter,
tableName: `${rootTableName}${adapter.localesSuffix}`,
@@ -614,6 +623,7 @@ export const getTableColumnFromPath = ({
fields: adapter.payload.collections[field.relationTo].config.flattenedFields,
joins,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
pathSegments: pathSegments.slice(1),
selectFields,
tableName: newTableName,
@@ -629,7 +639,7 @@ export const getTableColumnFromPath = ({
`${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`,
)
if (locale && field.localized && adapter.payload.config.localization) {
if (locale && isFieldLocalized && adapter.payload.config.localization) {
const conditions = [
eq(adapter.tables[tableName].id, adapter.tables[newTableName].parent),
eq(adapter.tables[newTableName]._locale, locale),
@@ -674,6 +684,7 @@ export const getTableColumnFromPath = ({
fields: field.flattenedFields,
joins,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
@@ -693,6 +704,7 @@ export const getTableColumnFromPath = ({
fields: field.flattenedFields,
joins,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
pathSegments: pathSegments.slice(1),
rootTableName,
selectFields,
@@ -711,7 +723,7 @@ export const getTableColumnFromPath = ({
let newTable = adapter.tables[newTableName]
if (field.localized && adapter.payload.config.localization) {
if (isFieldLocalized && adapter.payload.config.localization) {
// If localized, we go to localized table and set aliasTable to undefined
// so it is not picked up below to be used as targetTable
const parentTable = aliasTable || adapter.tables[tableName]

View File

@@ -20,6 +20,7 @@ type Args = {
fields: FlattenedField[]
joins: BuildQueryJoinAliases
locale: string
parentIsLocalized: boolean
selectFields: Record<string, GenericColumn>
selectLocale?: boolean
tableName: string
@@ -32,6 +33,7 @@ export function parseParams({
fields,
joins,
locale,
parentIsLocalized,
selectFields,
selectLocale,
tableName,
@@ -58,6 +60,7 @@ export function parseParams({
fields,
joins,
locale,
parentIsLocalized,
selectFields,
selectLocale,
tableName,
@@ -92,6 +95,7 @@ export function parseParams({
fields,
joins,
locale,
parentIsLocalized,
pathSegments: relationOrPath.replace(/__/g, '.').split('.'),
selectFields,
selectLocale,

View File

@@ -37,6 +37,7 @@ type Args = {
disableRelsTableUnique?: boolean
disableUnique: boolean
fields: FlattenedField[]
parentIsLocalized: boolean
rootRelationships?: Set<string>
rootRelationsToBuild?: RelationMap
rootTableIDColType?: IDType
@@ -71,6 +72,7 @@ export const buildTable = ({
disableRelsTableUnique = false,
disableUnique = false,
fields,
parentIsLocalized,
rootRelationships,
rootRelationsToBuild,
rootTableIDColType,
@@ -124,6 +126,7 @@ export const buildTable = ({
localesColumns,
localesIndexes,
newTableName: tableName,
parentIsLocalized,
parentTableName: tableName,
relationships,
relationsToBuild,

View File

@@ -55,6 +55,7 @@ export const buildRawSchema = ({
disableNotNull: !!collection?.versions?.drafts,
disableUnique: false,
fields: collection.flattenedFields,
parentIsLocalized: false,
setColumnID,
tableName,
timestamps: collection.timestamps,
@@ -72,6 +73,7 @@ export const buildRawSchema = ({
disableNotNull: !!collection.versions?.drafts,
disableUnique: true,
fields: versionFields,
parentIsLocalized: false,
setColumnID,
tableName: versionsTableName,
timestamps: true,
@@ -91,6 +93,7 @@ export const buildRawSchema = ({
disableNotNull: !!global?.versions?.drafts,
disableUnique: false,
fields: global.flattenedFields,
parentIsLocalized: false,
setColumnID,
tableName,
timestamps: false,
@@ -112,6 +115,7 @@ export const buildRawSchema = ({
disableNotNull: !!global.versions?.drafts,
disableUnique: true,
fields: versionFields,
parentIsLocalized: false,
setColumnID,
tableName: versionsTableName,
timestamps: true,

View File

@@ -1,7 +1,12 @@
import type { FlattenedField } from 'payload'
import { InvalidConfiguration } from 'payload'
import { fieldAffectsData, fieldIsVirtual, optionIsObject } from 'payload/shared'
import {
fieldAffectsData,
fieldIsVirtual,
fieldShouldBeLocalized,
optionIsObject,
} from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type {
@@ -37,6 +42,7 @@ type Args = {
localesColumns: Record<string, RawColumn>
localesIndexes: Record<string, RawIndex>
newTableName: string
parentIsLocalized: boolean
parentTableName: string
relationships: Set<string>
relationsToBuild: RelationMap
@@ -76,6 +82,7 @@ export const traverseFields = ({
localesColumns,
localesIndexes,
newTableName,
parentIsLocalized,
parentTableName,
relationships,
relationsToBuild,
@@ -119,11 +126,13 @@ export const traverseFields = ({
)}`
const fieldName = `${fieldPrefix?.replace('.', '_') || ''}${field.name}`
const isFieldLocalized = fieldShouldBeLocalized({ field, parentIsLocalized })
// If field is localized,
// add the column to the locale table instead of main table
if (
adapter.payload.config.localization &&
(field.localized || forceLocalized) &&
(isFieldLocalized || forceLocalized) &&
field.type !== 'array' &&
field.type !== 'blocks' &&
(('hasMany' in field && field.hasMany !== true) || !('hasMany' in field))
@@ -152,7 +161,7 @@ export const traverseFields = ({
targetIndexes[indexName] = {
name: indexName,
on: field.localized ? [fieldName, '_locale'] : fieldName,
on: isFieldLocalized ? [fieldName, '_locale'] : fieldName,
unique,
}
}
@@ -209,7 +218,7 @@ export const traverseFields = ({
}
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
Boolean(isFieldLocalized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
@@ -243,6 +252,7 @@ export const traverseFields = ({
disableRelsTableUnique: true,
disableUnique,
fields: disableUnique ? idToUUID(field.flattenedFields) : field.flattenedFields,
parentIsLocalized: parentIsLocalized || field.localized,
rootRelationships: relationships,
rootRelationsToBuild,
rootTableIDColType,
@@ -299,7 +309,12 @@ export const traverseFields = ({
},
}
if (hasLocalesTable(field.fields)) {
if (
hasLocalesTable({
fields: field.fields,
parentIsLocalized: parentIsLocalized || field.localized,
})
) {
arrayRelations._locales = {
type: 'many',
relationName: '_locales',
@@ -403,7 +418,7 @@ export const traverseFields = ({
}
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
Boolean(isFieldLocalized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
@@ -437,6 +452,7 @@ export const traverseFields = ({
disableRelsTableUnique: true,
disableUnique,
fields: disableUnique ? idToUUID(block.flattenedFields) : block.flattenedFields,
parentIsLocalized: parentIsLocalized || field.localized,
rootRelationships: relationships,
rootRelationsToBuild,
rootTableIDColType,
@@ -487,7 +503,12 @@ export const traverseFields = ({
},
}
if (hasLocalesTable(block.fields)) {
if (
hasLocalesTable({
fields: block.fields,
parentIsLocalized: parentIsLocalized || field.localized,
})
) {
blockRelations._locales = {
type: 'many',
relationName: '_locales',
@@ -529,6 +550,7 @@ export const traverseFields = ({
validateExistingBlockIsIdentical({
block,
localized: field.localized,
parentIsLocalized: parentIsLocalized || field.localized,
rootTableName,
table: adapter.rawTables[blockTableName],
tableLocales: adapter.rawTables[`${blockTableName}${adapter.localesSuffix}`],
@@ -605,11 +627,12 @@ export const traverseFields = ({
disableUnique,
fieldPrefix: `${fieldName}.`,
fields: field.flattenedFields,
forceLocalized: field.localized,
forceLocalized: isFieldLocalized,
indexes,
localesColumns,
localesIndexes,
newTableName: `${parentTableName}_${columnName}`,
parentIsLocalized: parentIsLocalized || field.localized,
parentTableName,
relationships,
relationsToBuild,
@@ -619,7 +642,7 @@ export const traverseFields = ({
setColumnID,
uniqueRelationships,
versions,
withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized,
withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || isFieldLocalized,
})
if (groupHasLocalizedField) {
@@ -659,7 +682,7 @@ export const traverseFields = ({
case 'number': {
if (field.hasMany) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
Boolean(isFieldLocalized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
@@ -784,7 +807,7 @@ export const traverseFields = ({
}
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
Boolean(isFieldLocalized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized
@@ -817,6 +840,7 @@ export const traverseFields = ({
disableNotNull,
disableUnique,
fields: [],
parentIsLocalized: parentIsLocalized || field.localized,
rootTableName,
setColumnID,
tableName: selectTableName,
@@ -904,7 +928,7 @@ export const traverseFields = ({
// add relationship to table
relationsToBuild.set(fieldName, {
type: 'one',
localized: adapter.payload.config.localization && (field.localized || forceLocalized),
localized: adapter.payload.config.localization && (isFieldLocalized || forceLocalized),
target: tableName,
})
@@ -916,7 +940,7 @@ export const traverseFields = ({
}
if (
Boolean(field.localized && adapter.payload.config.localization) ||
Boolean(isFieldLocalized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
) {
hasLocalizedRelationshipField = true
@@ -927,7 +951,7 @@ export const traverseFields = ({
case 'text': {
if (field.hasMany) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
Boolean(isFieldLocalized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock ||
forceLocalized

View File

@@ -14,6 +14,7 @@ type TransformArgs = {
fields: FlattenedField[]
joinQuery?: JoinQuery
locale?: string
parentIsLocalized: boolean
}
// This is the entry point to transform Drizzle output data
@@ -24,6 +25,7 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
data,
fields,
joinQuery,
parentIsLocalized,
}: TransformArgs): T => {
let relationships: Record<string, Record<string, unknown>[]> = {}
let texts: Record<string, Record<string, unknown>[]> = {}
@@ -59,6 +61,7 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
fields,
joinQuery,
numbers,
parentIsLocalized,
path: '',
relationships,
table: data,

View File

@@ -1,6 +1,6 @@
import type { FlattenedBlock, FlattenedField, JoinQuery, SanitizedConfig } from 'payload'
import { fieldIsVirtual } from 'payload/shared'
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
import type { DrizzleAdapter } from '../../types.js'
import type { BlocksMap } from '../../utilities/createBlocksMap.js'
@@ -46,6 +46,7 @@ type TraverseFieldsArgs = {
* All hasMany number fields, as returned by Drizzle, keyed on an object by field path
*/
numbers: Record<string, Record<string, unknown>[]>
parentIsLocalized: boolean
/**
* The current field path (in dot notation), used to merge in relationships
*/
@@ -80,6 +81,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
fields,
joinQuery,
numbers,
parentIsLocalized,
path,
relationships,
table,
@@ -105,9 +107,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions.push(() => delete table[fieldName])
}
const isLocalized = fieldShouldBeLocalized({ field, parentIsLocalized })
if (field.type === 'array') {
if (Array.isArray(fieldData)) {
if (field.localized) {
if (isLocalized) {
result[field.name] = fieldData.reduce((arrayResult, row) => {
if (typeof row._locale === 'string') {
if (!arrayResult[row._locale]) {
@@ -130,6 +134,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
fieldPrefix: '',
fields: field.flattenedFields,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${sanitizedPath}${field.name}.${row._order - 1}`,
relationships,
table: row,
@@ -175,6 +180,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
fieldPrefix: '',
fields: field.flattenedFields,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${sanitizedPath}${field.name}.${i}`,
relationships,
table: row,
@@ -197,7 +203,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
const blocksByPath = blocks[blockFieldPath]
if (Array.isArray(blocksByPath)) {
if (field.localized) {
if (isLocalized) {
result[field.name] = {}
blocksByPath.forEach((row) => {
@@ -232,6 +238,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
fieldPrefix: '',
fields: block.flattenedFields,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${blockFieldPath}.${row._order - 1}`,
relationships,
table: row,
@@ -303,6 +310,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
fieldPrefix: '',
fields: block.flattenedFields,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${blockFieldPath}.${i}`,
relationships,
table: row,
@@ -328,7 +336,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
if (field.type === 'relationship' || field.type === 'upload') {
if (typeof field.relationTo === 'string' && !('hasMany' in field && field.hasMany)) {
if (
field.localized &&
isLocalized &&
config.localization &&
config.localization.locales &&
Array.isArray(table?._locales)
@@ -344,7 +352,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
if (!relationPathMatch) {
if ('hasMany' in field && field.hasMany) {
if (field.localized && config.localization && config.localization.locales) {
if (isLocalized && config.localization && config.localization.locales) {
result[field.name] = {
[config.localization.defaultLocale]: [],
}
@@ -356,7 +364,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
return result
}
if (field.localized) {
if (isLocalized) {
result[field.name] = {}
const relationsByLocale: Record<string, Record<string, unknown>[]> = {}
@@ -402,7 +410,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
| { docs: unknown[]; hasNextPage: boolean }
| Record<string, { docs: unknown[]; hasNextPage: boolean }>
if (Array.isArray(fieldData)) {
if (field.localized && adapter.payload.config.localization) {
if (isLocalized && adapter.payload.config.localization) {
fieldResult = fieldData.reduce(
(joinResult, row) => {
if (typeof row.locale === 'string') {
@@ -446,7 +454,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
return result
}
if (field.localized) {
if (isLocalized) {
result[field.name] = {}
const textsByLocale: Record<string, Record<string, unknown>[]> = {}
@@ -485,7 +493,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
return result
}
if (field.localized) {
if (isLocalized) {
result[field.name] = {}
const numbersByLocale: Record<string, Record<string, unknown>[]> = {}
@@ -520,7 +528,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
if (field.type === 'select' && field.hasMany) {
if (Array.isArray(fieldData)) {
if (field.localized) {
if (isLocalized) {
result[field.name] = fieldData.reduce((selectResult, row) => {
if (typeof row.locale === 'string') {
if (!selectResult[row.locale]) {
@@ -542,7 +550,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
return result
}
if (field.localized && Array.isArray(table._locales)) {
if (isLocalized && Array.isArray(table._locales)) {
if (!table._locales.length && adapter.payload.config.localization) {
adapter.payload.config.localization.localeCodes.forEach((_locale) =>
(table._locales as unknown[]).push({ _locale }),
@@ -581,9 +589,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
const groupFieldPrefix = `${fieldPrefix || ''}${field.name}_`
const groupData = {}
const locale = table._locale as string
const refKey = field.localized && locale ? locale : field.name
const refKey = isLocalized && locale ? locale : field.name
if (field.localized && locale) {
if (isLocalized && locale) {
delete table._locale
}
ref[refKey] = traverseFields<Record<string, unknown>>({
@@ -595,6 +603,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
fieldPrefix: groupFieldPrefix,
fields: field.flattenedFields,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${sanitizedPath}${field.name}`,
relationships,
table,

View File

@@ -1,5 +1,7 @@
import type { FlattenedArrayField } from 'payload'
import { fieldShouldBeLocalized } from 'payload/shared'
import type { DrizzleAdapter } from '../../types.js'
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types.js'
@@ -18,6 +20,7 @@ type Args = {
field: FlattenedArrayField
locale?: string
numbers: Record<string, unknown>[]
parentIsLocalized: boolean
path: string
relationships: Record<string, unknown>[]
relationshipsToDelete: RelationshipToDelete[]
@@ -42,6 +45,7 @@ export const transformArray = ({
field,
locale,
numbers,
parentIsLocalized,
path,
relationships,
relationshipsToDelete,
@@ -79,7 +83,7 @@ export const transformArray = ({
}
}
if (field.localized) {
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && locale) {
newRow.row._locale = locale
}
@@ -100,6 +104,7 @@ export const transformArray = ({
insideArrayOrBlock: true,
locales: newRow.locales,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
parentTableName: arrayTableName,
path: `${path || ''}${field.name}.${i}.`,
relationships,

View File

@@ -1,5 +1,6 @@
import type { FlattenedBlock, FlattenedBlocksField } from 'payload'
import { fieldShouldBeLocalized } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from '../../types.js'
@@ -18,6 +19,7 @@ type Args = {
field: FlattenedBlocksField
locale?: string
numbers: Record<string, unknown>[]
parentIsLocalized: boolean
path: string
relationships: Record<string, unknown>[]
relationshipsToDelete: RelationshipToDelete[]
@@ -40,6 +42,7 @@ export const transformBlocks = ({
field,
locale,
numbers,
parentIsLocalized,
path,
relationships,
relationshipsToDelete,
@@ -76,7 +79,7 @@ export const transformBlocks = ({
},
}
if (field.localized && locale) {
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && locale) {
newRow.row._locale = locale
}
if (withinArrayOrBlockLocale) {
@@ -110,6 +113,7 @@ export const transformBlocks = ({
insideArrayOrBlock: true,
locales: newRow.locales,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
parentTableName: blockTableName,
path: `${path || ''}${field.name}.${i}.`,
relationships,

View File

@@ -9,6 +9,7 @@ type Args = {
adapter: DrizzleAdapter
data: Record<string, unknown>
fields: FlattenedField[]
parentIsLocalized: boolean
path?: string
tableName: string
}
@@ -17,6 +18,7 @@ export const transformForWrite = ({
adapter,
data,
fields,
parentIsLocalized,
path = '',
tableName,
}: Args): RowToInsert => {
@@ -48,6 +50,7 @@ export const transformForWrite = ({
fields,
locales: rowToInsert.locales,
numbers: rowToInsert.numbers,
parentIsLocalized,
parentTableName: tableName,
path,
relationships: rowToInsert.relationships,

View File

@@ -1,7 +1,7 @@
import type { FlattenedField } from 'payload'
import { sql } from 'drizzle-orm'
import { fieldIsVirtual } from 'payload/shared'
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from '../../types.js'
@@ -50,6 +50,7 @@ type Args = {
[locale: string]: Record<string, unknown>
}
numbers: Record<string, unknown>[]
parentIsLocalized: boolean
/**
* This is the name of the parent table
*/
@@ -84,6 +85,7 @@ export const traverseFields = ({
insideArrayOrBlock = false,
locales,
numbers,
parentIsLocalized,
parentTableName,
path,
relationships,
@@ -110,6 +112,8 @@ export const traverseFields = ({
fieldName = `${fieldPrefix || ''}${field.name}`
fieldData = data[field.name]
const isLocalized = fieldShouldBeLocalized({ field, parentIsLocalized })
if (field.type === 'array') {
const arrayTableName = adapter.tableNameMap.get(`${parentTableName}_${columnName}`)
@@ -117,7 +121,7 @@ export const traverseFields = ({
arrays[arrayTableName] = []
}
if (field.localized) {
if (isLocalized) {
if (typeof data[field.name] === 'object' && data[field.name] !== null) {
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
@@ -131,6 +135,7 @@ export const traverseFields = ({
field,
locale: localeKey,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path,
relationships,
relationshipsToDelete,
@@ -153,6 +158,7 @@ export const traverseFields = ({
data: data[field.name],
field,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path,
relationships,
relationshipsToDelete,
@@ -172,7 +178,7 @@ export const traverseFields = ({
blocksToDelete.add(toSnakeCase(typeof block === 'string' ? block : block.slug))
})
if (field.localized) {
if (isLocalized) {
if (typeof data[field.name] === 'object' && data[field.name] !== null) {
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
@@ -185,6 +191,7 @@ export const traverseFields = ({
field,
locale: localeKey,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path,
relationships,
relationshipsToDelete,
@@ -204,6 +211,7 @@ export const traverseFields = ({
data: fieldData,
field,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path,
relationships,
relationshipsToDelete,
@@ -218,7 +226,7 @@ export const traverseFields = ({
if (field.type === 'group' || field.type === 'tab') {
if (typeof data[field.name] === 'object' && data[field.name] !== null) {
if (field.localized) {
if (isLocalized) {
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
// preserve array ID if there is
localeData._uuid = data.id || data._uuid
@@ -238,6 +246,7 @@ export const traverseFields = ({
insideArrayOrBlock,
locales,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
parentTableName,
path: `${path || ''}${field.name}.`,
relationships,
@@ -267,6 +276,7 @@ export const traverseFields = ({
insideArrayOrBlock,
locales,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
parentTableName,
path: `${path || ''}${field.name}.`,
relationships,
@@ -286,7 +296,7 @@ export const traverseFields = ({
const relationshipPath = `${path || ''}${field.name}`
if (
field.localized &&
isLocalized &&
(Array.isArray(field.relationTo) || ('hasMany' in field && field.hasMany))
) {
if (typeof fieldData === 'object') {
@@ -329,14 +339,14 @@ export const traverseFields = ({
return
} else {
if (
!field.localized &&
!isLocalized &&
fieldData &&
typeof fieldData === 'object' &&
'id' in fieldData &&
fieldData?.id
) {
fieldData = fieldData.id
} else if (field.localized) {
} else if (isLocalized) {
if (typeof fieldData === 'object') {
Object.entries(fieldData).forEach(([localeKey, localeData]) => {
if (typeof localeData === 'object') {
@@ -355,7 +365,7 @@ export const traverseFields = ({
if (field.type === 'text' && field.hasMany) {
const textPath = `${path || ''}${field.name}`
if (field.localized) {
if (isLocalized) {
if (typeof fieldData === 'object') {
Object.entries(fieldData).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
@@ -387,7 +397,7 @@ export const traverseFields = ({
if (field.type === 'number' && field.hasMany) {
const numberPath = `${path || ''}${field.name}`
if (field.localized) {
if (isLocalized) {
if (typeof fieldData === 'object') {
Object.entries(fieldData).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
@@ -422,7 +432,7 @@ export const traverseFields = ({
selects[selectTableName] = []
}
if (field.localized) {
if (isLocalized) {
if (typeof data[field.name] === 'object' && data[field.name] !== null) {
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
@@ -451,7 +461,7 @@ export const traverseFields = ({
const valuesToTransform: { localeKey?: string; ref: unknown; value: unknown }[] = []
if (field.localized) {
if (isLocalized) {
if (typeof fieldData === 'object' && fieldData !== null) {
Object.entries(fieldData).forEach(([localeKey, localeData]) => {
if (!locales[localeKey]) {

View File

@@ -38,6 +38,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
adapter,
data,
fields,
parentIsLocalized: false,
path,
tableName,
})
@@ -459,6 +460,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
data: doc,
fields,
joinQuery: false,
parentIsLocalized: false,
})
return result

View File

@@ -1,21 +1,38 @@
import type { Field } from 'payload'
import { fieldAffectsData, fieldHasSubFields } from 'payload/shared'
import { fieldAffectsData, fieldHasSubFields, fieldShouldBeLocalized } from 'payload/shared'
export const hasLocalesTable = (fields: Field[]): boolean => {
export const hasLocalesTable = ({
fields,
parentIsLocalized,
}: {
fields: Field[]
/**
* @todo make required in v4.0. Usually you'd wanna pass this in
*/
parentIsLocalized?: boolean
}): boolean => {
return fields.some((field) => {
// arrays always get a separate table
if (field.type === 'array') {
return false
}
if (fieldAffectsData(field) && field.localized) {
if (fieldAffectsData(field) && fieldShouldBeLocalized({ field, parentIsLocalized })) {
return true
}
if (fieldHasSubFields(field)) {
return hasLocalesTable(field.fields)
return hasLocalesTable({
fields: field.fields,
parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized),
})
}
if (field.type === 'tabs') {
return field.tabs.some((tab) => hasLocalesTable(tab.fields))
return field.tabs.some((tab) =>
hasLocalesTable({
fields: tab.fields,
parentIsLocalized: parentIsLocalized || tab.localized,
}),
)
}
return false
})

View File

@@ -1,22 +1,33 @@
import type { Block, Field } from 'payload'
import { InvalidConfiguration } from 'payload'
import { fieldAffectsData, fieldHasSubFields, tabHasName } from 'payload/shared'
import {
fieldAffectsData,
fieldHasSubFields,
fieldShouldBeLocalized,
tabHasName,
} from 'payload/shared'
import type { RawTable } from '../types.js'
type Args = {
block: Block
localized: boolean
/**
* @todo make required in v4.0. Usually you'd wanna pass this in
*/
parentIsLocalized?: boolean
rootTableName: string
table: RawTable
tableLocales?: RawTable
}
const getFlattenedFieldNames = (
fields: Field[],
prefix: string = '',
): { localized?: boolean; name: string }[] => {
const getFlattenedFieldNames = (args: {
fields: Field[]
parentIsLocalized: boolean
prefix?: string
}): { localized?: boolean; name: string }[] => {
const { fields, parentIsLocalized, prefix = '' } = args
return fields.reduce((fieldsToUse, field) => {
let fieldPrefix = prefix
@@ -29,7 +40,14 @@ const getFlattenedFieldNames = (
if (fieldHasSubFields(field)) {
fieldPrefix = 'name' in field ? `${prefix}${field.name}_` : prefix
return [...fieldsToUse, ...getFlattenedFieldNames(field.fields, fieldPrefix)]
return [
...fieldsToUse,
...getFlattenedFieldNames({
fields: field.fields,
parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized),
prefix: fieldPrefix,
}),
]
}
if (field.type === 'tabs') {
@@ -41,7 +59,11 @@ const getFlattenedFieldNames = (
...tabFields,
...(tabHasName(tab)
? [{ ...tab, type: 'tab' }]
: getFlattenedFieldNames(tab.fields, fieldPrefix)),
: getFlattenedFieldNames({
fields: tab.fields,
parentIsLocalized: parentIsLocalized || tab.localized,
prefix: fieldPrefix,
})),
]
}, []),
]
@@ -52,7 +74,7 @@ const getFlattenedFieldNames = (
...fieldsToUse,
{
name: `${fieldPrefix}${field.name}`,
localized: field.localized,
localized: fieldShouldBeLocalized({ field, parentIsLocalized }),
},
]
}
@@ -64,11 +86,15 @@ const getFlattenedFieldNames = (
export const validateExistingBlockIsIdentical = ({
block,
localized,
parentIsLocalized,
rootTableName,
table,
tableLocales,
}: Args): void => {
const fieldNames = getFlattenedFieldNames(block.fields)
const fieldNames = getFlattenedFieldNames({
fields: block.fields,
parentIsLocalized: parentIsLocalized || localized,
})
const missingField =
// ensure every field from the config is in the matching table

View File

@@ -75,6 +75,7 @@ type BuildMutationInputTypeArgs = {
forceNullable?: boolean
graphqlResult: GraphQLInfo
name: string
parentIsLocalized: boolean
parentName: string
}
@@ -84,6 +85,7 @@ export function buildMutationInputType({
fields,
forceNullable = false,
graphqlResult,
parentIsLocalized,
parentName,
}: BuildMutationInputTypeArgs): GraphQLInputObjectType | null {
const fieldToSchemaMap = {
@@ -94,6 +96,7 @@ export function buildMutationInputType({
config,
fields: field.fields,
graphqlResult,
parentIsLocalized: parentIsLocalized || field.localized,
parentName: fullName,
})
@@ -101,7 +104,7 @@ export function buildMutationInputType({
return inputObjectTypeConfig
}
type = new GraphQLList(withNullableType(field, type, forceNullable))
type = new GraphQLList(withNullableType({ type, field, forceNullable, parentIsLocalized }))
return {
...inputObjectTypeConfig,
[field.name]: { type },
@@ -117,7 +120,9 @@ export function buildMutationInputType({
}),
code: (inputObjectTypeConfig: InputObjectTypeConfig, field: CodeField) => ({
...inputObjectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
[field.name]: {
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
},
}),
collapsible: (inputObjectTypeConfig: InputObjectTypeConfig, field: CollapsibleField) =>
field.fields.reduce((acc, subField: CollapsibleField) => {
@@ -129,11 +134,15 @@ export function buildMutationInputType({
}, inputObjectTypeConfig),
date: (inputObjectTypeConfig: InputObjectTypeConfig, field: DateField) => ({
...inputObjectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
[field.name]: {
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
},
}),
email: (inputObjectTypeConfig: InputObjectTypeConfig, field: EmailField) => ({
...inputObjectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
[field.name]: {
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
},
}),
group: (inputObjectTypeConfig: InputObjectTypeConfig, field: GroupField) => {
const requiresAtLeastOneField = groupOrTabHasRequiredSubfield(field)
@@ -143,6 +152,7 @@ export function buildMutationInputType({
config,
fields: field.fields,
graphqlResult,
parentIsLocalized: parentIsLocalized || field.localized,
parentName: fullName,
})
@@ -160,28 +170,40 @@ export function buildMutationInputType({
},
json: (inputObjectTypeConfig: InputObjectTypeConfig, field: JSONField) => ({
...inputObjectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLJSON, forceNullable) },
[field.name]: {
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
},
}),
number: (inputObjectTypeConfig: InputObjectTypeConfig, field: NumberField) => {
const type = field.name === 'id' ? GraphQLInt : GraphQLFloat
return {
...inputObjectTypeConfig,
[field.name]: {
type: withNullableType(
type: withNullableType({
type: field.hasMany === true ? new GraphQLList(type) : type,
field,
field.hasMany === true ? new GraphQLList(type) : type,
forceNullable,
),
parentIsLocalized,
}),
},
}
},
point: (inputObjectTypeConfig: InputObjectTypeConfig, field: PointField) => ({
...inputObjectTypeConfig,
[field.name]: { type: withNullableType(field, new GraphQLList(GraphQLFloat), forceNullable) },
[field.name]: {
type: withNullableType({
type: new GraphQLList(GraphQLFloat),
field,
forceNullable,
parentIsLocalized,
}),
},
}),
radio: (inputObjectTypeConfig: InputObjectTypeConfig, field: RadioField) => ({
...inputObjectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
[field.name]: {
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
},
}),
relationship: (inputObjectTypeConfig: InputObjectTypeConfig, field: RelationshipField) => {
const { relationTo } = field
@@ -230,7 +252,9 @@ export function buildMutationInputType({
},
richText: (inputObjectTypeConfig: InputObjectTypeConfig, field: RichTextField) => ({
...inputObjectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLJSON, forceNullable) },
[field.name]: {
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
},
}),
row: (inputObjectTypeConfig: InputObjectTypeConfig, field: RowField) =>
field.fields.reduce((acc, subField: Field) => {
@@ -264,7 +288,7 @@ export function buildMutationInputType({
})
type = field.hasMany ? new GraphQLList(type) : type
type = withNullableType(field, type, forceNullable)
type = withNullableType({ type, field, forceNullable, parentIsLocalized })
return {
...inputObjectTypeConfig,
@@ -281,6 +305,7 @@ export function buildMutationInputType({
config,
fields: tab.fields,
graphqlResult,
parentIsLocalized: parentIsLocalized || tab.localized,
parentName: fullName,
})
@@ -312,16 +337,19 @@ export function buildMutationInputType({
text: (inputObjectTypeConfig: InputObjectTypeConfig, field: TextField) => ({
...inputObjectTypeConfig,
[field.name]: {
type: withNullableType(
type: withNullableType({
type: field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
field,
field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
forceNullable,
),
parentIsLocalized,
}),
},
}),
textarea: (inputObjectTypeConfig: InputObjectTypeConfig, field: TextareaField) => ({
...inputObjectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
[field.name]: {
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
},
}),
upload: (inputObjectTypeConfig: InputObjectTypeConfig, field: UploadField) => {
const { relationTo } = field

View File

@@ -62,6 +62,7 @@ type Args = {
forceNullable?: boolean
graphqlResult: GraphQLInfo
name: string
parentIsLocalized?: boolean
parentName: string
}
@@ -72,6 +73,7 @@ export function buildObjectType({
fields,
forceNullable,
graphqlResult,
parentIsLocalized,
parentName,
}: Args): GraphQLObjectType {
const fieldToSchemaMap = {
@@ -84,8 +86,9 @@ export function buildObjectType({
name: interfaceName,
config,
fields: field.fields,
forceNullable: isFieldNullable(field, forceNullable),
forceNullable: isFieldNullable({ field, forceNullable, parentIsLocalized }),
graphqlResult,
parentIsLocalized: field.localized || parentIsLocalized,
parentName: interfaceName,
})
@@ -104,7 +107,7 @@ export function buildObjectType({
return {
...objectTypeConfig,
[field.name]: { type: withNullableType(field, arrayType) },
[field.name]: { type: withNullableType({ type: arrayType, field, parentIsLocalized }) },
}
},
blocks: (objectTypeConfig: ObjectTypeConfig, field: BlocksField) => {
@@ -132,6 +135,7 @@ export function buildObjectType({
],
forceNullable,
graphqlResult,
parentIsLocalized,
parentName: interfaceName,
})
@@ -165,16 +169,20 @@ export function buildObjectType({
return {
...objectTypeConfig,
[field.name]: { type: withNullableType(field, type) },
[field.name]: { type: withNullableType({ type, field, parentIsLocalized }) },
}
},
checkbox: (objectTypeConfig: ObjectTypeConfig, field: CheckboxField) => ({
...objectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLBoolean, forceNullable) },
[field.name]: {
type: withNullableType({ type: GraphQLBoolean, field, forceNullable, parentIsLocalized }),
},
}),
code: (objectTypeConfig: ObjectTypeConfig, field: CodeField) => ({
...objectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
[field.name]: {
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
},
}),
collapsible: (objectTypeConfig: ObjectTypeConfig, field: CollapsibleField) =>
field.fields.reduce((objectTypeConfigWithCollapsibleFields, subField) => {
@@ -186,11 +194,20 @@ export function buildObjectType({
}, objectTypeConfig),
date: (objectTypeConfig: ObjectTypeConfig, field: DateField) => ({
...objectTypeConfig,
[field.name]: { type: withNullableType(field, DateTimeResolver, forceNullable) },
[field.name]: {
type: withNullableType({ type: DateTimeResolver, field, forceNullable, parentIsLocalized }),
},
}),
email: (objectTypeConfig: ObjectTypeConfig, field: EmailField) => ({
...objectTypeConfig,
[field.name]: { type: withNullableType(field, EmailAddressResolver, forceNullable) },
[field.name]: {
type: withNullableType({
type: EmailAddressResolver,
field,
forceNullable,
parentIsLocalized,
}),
},
}),
group: (objectTypeConfig: ObjectTypeConfig, field: GroupField) => {
const interfaceName =
@@ -201,8 +218,9 @@ export function buildObjectType({
name: interfaceName,
config,
fields: field.fields,
forceNullable: isFieldNullable(field, forceNullable),
forceNullable: isFieldNullable({ field, forceNullable, parentIsLocalized }),
graphqlResult,
parentIsLocalized: field.localized || parentIsLocalized,
parentName: interfaceName,
})
@@ -286,42 +304,47 @@ export function buildObjectType({
},
json: (objectTypeConfig: ObjectTypeConfig, field: JSONField) => ({
...objectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLJSON, forceNullable) },
[field.name]: {
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
},
}),
number: (objectTypeConfig: ObjectTypeConfig, field: NumberField) => {
const type = field?.name === 'id' ? GraphQLInt : GraphQLFloat
return {
...objectTypeConfig,
[field.name]: {
type: withNullableType(
type: withNullableType({
type: field?.hasMany === true ? new GraphQLList(type) : type,
field,
field?.hasMany === true ? new GraphQLList(type) : type,
forceNullable,
),
parentIsLocalized,
}),
},
}
},
point: (objectTypeConfig: ObjectTypeConfig, field: PointField) => ({
...objectTypeConfig,
[field.name]: {
type: withNullableType(
type: withNullableType({
type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
field,
new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
forceNullable,
),
parentIsLocalized,
}),
},
}),
radio: (objectTypeConfig: ObjectTypeConfig, field: RadioField) => ({
...objectTypeConfig,
[field.name]: {
type: withNullableType(
field,
new GraphQLEnumType({
type: withNullableType({
type: new GraphQLEnumType({
name: combineParentName(parentName, field.name),
values: formatOptions(field),
}),
field,
forceNullable,
),
parentIsLocalized,
}),
},
}),
relationship: (objectTypeConfig: ObjectTypeConfig, field: RelationshipField) => {
@@ -420,11 +443,12 @@ export function buildObjectType({
}
const relationship = {
type: withNullableType(
type: withNullableType({
type: hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
field,
hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
forceNullable,
),
parentIsLocalized,
}),
args: relationshipArgs,
extensions: {
complexity:
@@ -550,7 +574,7 @@ export function buildObjectType({
richText: (objectTypeConfig: ObjectTypeConfig, field: RichTextField) => ({
...objectTypeConfig,
[field.name]: {
type: withNullableType(field, GraphQLJSON, forceNullable),
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
args: {
depth: {
type: GraphQLInt,
@@ -591,6 +615,7 @@ export function buildObjectType({
findMany: false,
flattenLocales: false,
overrideAccess: false,
parentIsLocalized,
populationPromises,
req: context.req,
showHiddenFields: false,
@@ -621,7 +646,7 @@ export function buildObjectType({
})
type = field.hasMany ? new GraphQLList(new GraphQLNonNull(type)) : type
type = withNullableType(field, type, forceNullable)
type = withNullableType({ type, field, forceNullable, parentIsLocalized })
return {
...objectTypeConfig,
@@ -641,6 +666,7 @@ export function buildObjectType({
fields: tab.fields,
forceNullable,
graphqlResult,
parentIsLocalized: tab.localized || parentIsLocalized,
parentName: interfaceName,
})
@@ -681,16 +707,19 @@ export function buildObjectType({
text: (objectTypeConfig: ObjectTypeConfig, field: TextField) => ({
...objectTypeConfig,
[field.name]: {
type: withNullableType(
type: withNullableType({
type: field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
field,
field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
forceNullable,
),
parentIsLocalized,
}),
},
}),
textarea: (objectTypeConfig: ObjectTypeConfig, field: TextareaField) => ({
...objectTypeConfig,
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
[field.name]: {
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
},
}),
upload: (objectTypeConfig: ObjectTypeConfig, field: UploadField) => {
const { relationTo } = field
@@ -775,11 +804,12 @@ export function buildObjectType({
}
const relationship = {
type: withNullableType(
type: withNullableType({
type: hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
field,
hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
forceNullable,
),
parentIsLocalized,
}),
args: relationshipArgs,
extensions: {
complexity:

View File

@@ -150,6 +150,7 @@ export function initCollections({ config, graphqlResult }: InitCollectionsGraphQ
config,
fields: mutationInputFields,
graphqlResult,
parentIsLocalized: false,
parentName: singularName,
})
if (createMutationInputType) {
@@ -164,6 +165,7 @@ export function initCollections({ config, graphqlResult }: InitCollectionsGraphQ
),
forceNullable: true,
graphqlResult,
parentIsLocalized: false,
parentName: `${singularName}Update`,
})
if (updateMutationInputType) {

View File

@@ -45,6 +45,7 @@ export function initGlobals({ config, graphqlResult }: InitGlobalsGraphQLArgs):
config,
fields,
graphqlResult,
parentIsLocalized: false,
parentName: formattedName,
})
graphqlResult.globals.graphQL[slug] = {

View File

@@ -2,15 +2,23 @@ import type { FieldAffectingData } from 'payload'
import { fieldAffectsData } from 'payload/shared'
export const isFieldNullable = (field: FieldAffectingData, force: boolean): boolean => {
export const isFieldNullable = ({
field,
forceNullable,
parentIsLocalized,
}: {
field: FieldAffectingData
forceNullable: boolean
parentIsLocalized: boolean
}): boolean => {
const hasReadAccessControl = field.access && field.access.read
const condition = field.admin && field.admin.condition
return !(
force &&
forceNullable &&
fieldAffectsData(field) &&
'required' in field &&
field.required &&
!field.localized &&
(!field.localized || parentIsLocalized) &&
!condition &&
!hasReadAccessControl
)

View File

@@ -3,11 +3,17 @@ import type { FieldAffectingData } from 'payload'
import { GraphQLNonNull } from 'graphql'
export const withNullableType = (
field: FieldAffectingData,
type: GraphQLType,
forceNullable = false,
): GraphQLType => {
export const withNullableType = ({
type,
field,
forceNullable,
parentIsLocalized,
}: {
field: FieldAffectingData
forceNullable?: boolean
parentIsLocalized: boolean
type: GraphQLType
}): GraphQLType => {
const hasReadAccessControl = field.access && field.access.read
const condition = field.admin && field.admin.condition
const isTimestamp = field.name === 'createdAt' || field.name === 'updatedAt'
@@ -16,7 +22,7 @@ export const withNullableType = (
!forceNullable &&
'required' in field &&
field.required &&
!field.localized &&
(!field.localized || parentIsLocalized) &&
!condition &&
!hasReadAccessControl &&
!isTimestamp

View File

@@ -22,6 +22,7 @@ type Props =
isIterable?: false
label: React.ReactNode
locales: string[] | undefined
parentIsLocalized: boolean
version: unknown
}
| {
@@ -34,6 +35,7 @@ type Props =
isIterable: true
label: React.ReactNode
locales: string[] | undefined
parentIsLocalized: boolean
version: unknown
}
@@ -46,6 +48,7 @@ export const DiffCollapser: React.FC<Props> = ({
isIterable = false,
label,
locales,
parentIsLocalized,
version,
}) => {
const { t } = useTranslation()
@@ -74,6 +77,7 @@ export const DiffCollapser: React.FC<Props> = ({
config,
field,
locales,
parentIsLocalized,
versionRows,
})
} else {
@@ -82,6 +86,7 @@ export const DiffCollapser: React.FC<Props> = ({
config,
fields,
locales,
parentIsLocalized,
version,
})
}

View File

@@ -17,7 +17,7 @@ import type { DiffMethod } from 'react-diff-viewer-continued'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { dequal } from 'dequal/lite'
import { fieldIsID, getUniqueListBy, tabHasName } from 'payload/shared'
import { fieldIsID, fieldShouldBeLocalized, getUniqueListBy, tabHasName } from 'payload/shared'
import { diffMethods } from './fields/diffMethods.js'
import { diffComponents } from './fields/index.js'
@@ -39,6 +39,7 @@ export type BuildVersionFieldsArgs = {
i18n: I18nClient
modifiedOnly: boolean
parentIndexPath: string
parentIsLocalized: boolean
parentPath: string
parentSchemaPath: string
req: PayloadRequest
@@ -63,6 +64,7 @@ export const buildVersionFields = ({
i18n,
modifiedOnly,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
req,
@@ -103,7 +105,8 @@ export const buildVersionFields = ({
}
const versionField: VersionField = {}
const isLocalized = 'localized' in field && field.localized
const isLocalized = fieldShouldBeLocalized({ field, parentIsLocalized })
const fieldName: null | string = 'name' in field ? field.name : null
const versionValue = fieldName ? versionSiblingData?.[fieldName] : versionSiblingData
@@ -126,6 +129,7 @@ export const buildVersionFields = ({
indexPath,
locale,
modifiedOnly,
parentIsLocalized: true,
parentPath,
parentSchemaPath,
path,
@@ -150,6 +154,7 @@ export const buildVersionFields = ({
i18n,
indexPath,
modifiedOnly,
parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized),
parentPath,
parentSchemaPath,
path,
@@ -184,6 +189,7 @@ const buildVersionField = ({
indexPath,
locale,
modifiedOnly,
parentIsLocalized,
parentPath,
parentSchemaPath,
path,
@@ -198,6 +204,7 @@ const buildVersionField = ({
indexPath: string
locale?: string
modifiedOnly?: boolean
parentIsLocalized: boolean
path: string
schemaPath: string
versionValue: unknown
@@ -272,6 +279,7 @@ const buildVersionField = ({
i18n,
modifiedOnly,
parentIndexPath: isNamedTab ? '' : tabIndexPath,
parentIsLocalized: parentIsLocalized || tab.localized,
parentPath: tabPath,
parentSchemaPath: tabSchemaPath,
req,
@@ -300,6 +308,7 @@ const buildVersionField = ({
i18n,
modifiedOnly,
parentIndexPath: 'name' in field ? '' : indexPath,
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + i,
parentSchemaPath: schemaPath,
req,
@@ -318,6 +327,7 @@ const buildVersionField = ({
i18n,
modifiedOnly,
parentIndexPath: 'name' in field ? '' : indexPath,
parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized),
parentPath: path,
parentSchemaPath: schemaPath,
req,
@@ -374,6 +384,7 @@ const buildVersionField = ({
i18n,
modifiedOnly,
parentIndexPath: 'name' in field ? '' : indexPath,
parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized),
parentPath: path + '.' + i,
parentSchemaPath: schemaPath + '.' + versionBlock.slug,
req,
@@ -392,6 +403,7 @@ const buildVersionField = ({
diffMethod,
field: clientField,
fieldPermissions: subFieldPermissions,
parentIsLocalized,
versionValue,
}

View File

@@ -15,6 +15,7 @@ export const Collapsible: CollapsibleFieldDiffClientComponent = ({
baseVersionField,
comparisonValue,
field,
parentIsLocalized,
versionValue,
}) => {
const { i18n } = useTranslation()
@@ -35,6 +36,7 @@ export const Collapsible: CollapsibleFieldDiffClientComponent = ({
typeof field.label !== 'function' && <span>{getTranslation(field.label, i18n)}</span>
}
locales={selectedLocales}
parentIsLocalized={parentIsLocalized || field.localized}
version={versionValue}
>
<RenderVersionFieldsToDiff versionFields={baseVersionField.fields} />

View File

@@ -19,6 +19,7 @@ export const Group: GroupFieldDiffClientComponent = ({
comparisonValue,
field,
locale,
parentIsLocalized,
versionValue,
}) => {
const { i18n } = useTranslation()
@@ -40,6 +41,7 @@ export const Group: GroupFieldDiffClientComponent = ({
)
}
locales={selectedLocales}
parentIsLocalized={parentIsLocalized || field.localized}
version={versionValue}
>
<RenderVersionFieldsToDiff versionFields={baseVersionField.fields} />

View File

@@ -22,6 +22,7 @@ export const Iterable: React.FC<FieldDiffClientProps> = ({
comparisonValue,
field,
locale,
parentIsLocalized,
versionValue,
}) => {
const { i18n } = useTranslation()
@@ -53,6 +54,7 @@ export const Iterable: React.FC<FieldDiffClientProps> = ({
)
}
locales={selectedLocales}
parentIsLocalized={parentIsLocalized}
version={versionValue}
>
{maxRows > 0 && (
@@ -80,6 +82,7 @@ export const Iterable: React.FC<FieldDiffClientProps> = ({
fields={fields}
label={rowLabel}
locales={selectedLocales}
parentIsLocalized={parentIsLocalized || field.localized}
version={versionRow}
>
<RenderVersionFieldsToDiff versionFields={versionFields} />

View File

@@ -1,13 +1,14 @@
'use client'
import type {
ClientCollectionConfig,
ClientConfig,
ClientField,
RelationshipFieldDiffClientComponent,
} from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { useConfig, useTranslation } from '@payloadcms/ui'
import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared'
import { fieldAffectsData, fieldIsPresentationalOnly, fieldShouldBeLocalized } from 'payload/shared'
import React from 'react'
import ReactDiffViewer from 'react-diff-viewer-continued'
@@ -24,10 +25,12 @@ const generateLabelFromValue = (
field: ClientField,
locale: string,
value: { relationTo: string; value: RelationshipValue } | RelationshipValue,
config: ClientConfig,
parentIsLocalized: boolean,
): string => {
if (Array.isArray(value)) {
return value
.map((v) => generateLabelFromValue(collections, field, locale, v))
.map((v) => generateLabelFromValue(collections, field, locale, v, config, parentIsLocalized))
.filter(Boolean) // Filters out any undefined or empty values
.join(', ')
}
@@ -65,7 +68,7 @@ const generateLabelFromValue = (
let titleFieldIsLocalized = false
if (useAsTitleField && fieldAffectsData(useAsTitleField)) {
titleFieldIsLocalized = useAsTitleField.localized
titleFieldIsLocalized = fieldShouldBeLocalized({ field: useAsTitleField, parentIsLocalized })
}
if (typeof relatedDoc?.[useAsTitle] !== 'undefined') {
@@ -102,9 +105,11 @@ export const Relationship: RelationshipFieldDiffClientComponent = ({
comparisonValue,
field,
locale,
parentIsLocalized,
versionValue,
}) => {
const { i18n } = useTranslation()
const { config } = useConfig()
const placeholder = `[${i18n.t('general:noValue')}]`
@@ -119,11 +124,20 @@ export const Relationship: RelationshipFieldDiffClientComponent = ({
if ('hasMany' in field && field.hasMany && Array.isArray(versionValue)) {
versionToRender =
versionValue
.map((val) => generateLabelFromValue(collections, field, locale, val))
.map((val) =>
generateLabelFromValue(collections, field, locale, val, config, parentIsLocalized),
)
.join(', ') || placeholder
} else {
versionToRender =
generateLabelFromValue(collections, field, locale, versionValue) || placeholder
generateLabelFromValue(
collections,
field,
locale,
versionValue,
config,
parentIsLocalized,
) || placeholder
}
}
@@ -131,11 +145,20 @@ export const Relationship: RelationshipFieldDiffClientComponent = ({
if ('hasMany' in field && field.hasMany && Array.isArray(comparisonValue)) {
comparisonToRender =
comparisonValue
.map((val) => generateLabelFromValue(collections, field, locale, val))
.map((val) =>
generateLabelFromValue(collections, field, locale, val, config, parentIsLocalized),
)
.join(', ') || placeholder
} else {
comparisonToRender =
generateLabelFromValue(collections, field, locale, comparisonValue) || placeholder
generateLabelFromValue(
collections,
field,
locale,
comparisonValue,
config,
parentIsLocalized,
) || placeholder
}
}

View File

@@ -79,7 +79,14 @@ type TabProps = {
tab: VersionTab
} & FieldDiffClientProps<TabsFieldClient>
const Tab: React.FC<TabProps> = ({ comparisonValue, fieldTab, locale, tab, versionValue }) => {
const Tab: React.FC<TabProps> = ({
comparisonValue,
fieldTab,
locale,
parentIsLocalized,
tab,
versionValue,
}) => {
const { i18n } = useTranslation()
const { selectedLocales } = useSelectedLocales()
@@ -102,6 +109,7 @@ const Tab: React.FC<TabProps> = ({ comparisonValue, fieldTab, locale, tab, versi
)
}
locales={selectedLocales}
parentIsLocalized={parentIsLocalized || fieldTab.localized}
version={versionValue}
>
<RenderVersionFieldsToDiff versionFields={tab.fields} />

View File

@@ -1,5 +1,7 @@
import type { ArrayFieldClient, BlocksFieldClient, ClientConfig, ClientField } from 'payload'
import { fieldShouldBeLocalized } from 'payload/shared'
import { fieldHasChanges } from './fieldHasChanges.js'
import { getFieldsForRowComparison } from './getFieldsForRowComparison.js'
@@ -8,6 +10,7 @@ type Args = {
config: ClientConfig
fields: ClientField[]
locales: string[] | undefined
parentIsLocalized: boolean
version: unknown
}
@@ -15,7 +18,14 @@ type Args = {
* Recursively counts the number of changed fields between comparison and
* version data for a given set of fields.
*/
export function countChangedFields({ comparison, config, fields, locales, version }: Args) {
export function countChangedFields({
comparison,
config,
fields,
locales,
parentIsLocalized,
version,
}: Args) {
let count = 0
fields.forEach((field) => {
@@ -29,7 +39,7 @@ export function countChangedFields({ comparison, config, fields, locales, versio
// count the number of changed fields in each.
case 'array':
case 'blocks': {
if (locales && field.localized) {
if (locales && fieldShouldBeLocalized({ field, parentIsLocalized })) {
locales.forEach((locale) => {
const comparisonRows = comparison?.[field.name]?.[locale] ?? []
const versionRows = version?.[field.name]?.[locale] ?? []
@@ -38,13 +48,21 @@ export function countChangedFields({ comparison, config, fields, locales, versio
config,
field,
locales,
parentIsLocalized: parentIsLocalized || field.localized,
versionRows,
})
})
} else {
const comparisonRows = comparison?.[field.name] ?? []
const versionRows = version?.[field.name] ?? []
count += countChangedFieldsInRows({ comparisonRows, config, field, locales, versionRows })
count += countChangedFieldsInRows({
comparisonRows,
config,
field,
locales,
parentIsLocalized: parentIsLocalized || field.localized,
versionRows,
})
}
break
}
@@ -66,7 +84,7 @@ export function countChangedFields({ comparison, config, fields, locales, versio
case 'textarea':
case 'upload': {
// Fields that have a name and contain data. We can just check if the data has changed.
if (locales && field.localized) {
if (locales && fieldShouldBeLocalized({ field, parentIsLocalized })) {
locales.forEach((locale) => {
if (
fieldHasChanges(version?.[field.name]?.[locale], comparison?.[field.name]?.[locale])
@@ -87,6 +105,7 @@ export function countChangedFields({ comparison, config, fields, locales, versio
config,
fields: field.fields,
locales,
parentIsLocalized: parentIsLocalized || field.localized,
version,
})
@@ -95,13 +114,14 @@ export function countChangedFields({ comparison, config, fields, locales, versio
// Fields that have nested fields and nest their fields' data.
case 'group': {
if (locales && field.localized) {
if (locales && fieldShouldBeLocalized({ field, parentIsLocalized })) {
locales.forEach((locale) => {
count += countChangedFields({
comparison: comparison?.[field.name]?.[locale],
config,
fields: field.fields,
locales,
parentIsLocalized: parentIsLocalized || field.localized,
version: version?.[field.name]?.[locale],
})
})
@@ -111,6 +131,7 @@ export function countChangedFields({ comparison, config, fields, locales, versio
config,
fields: field.fields,
locales,
parentIsLocalized: parentIsLocalized || field.localized,
version: version?.[field.name],
})
}
@@ -129,6 +150,7 @@ export function countChangedFields({ comparison, config, fields, locales, versio
config,
fields: tab.fields,
locales,
parentIsLocalized: parentIsLocalized || tab.localized,
version: version?.[tab.name]?.[locale],
})
})
@@ -139,6 +161,7 @@ export function countChangedFields({ comparison, config, fields, locales, versio
config,
fields: tab.fields,
locales,
parentIsLocalized: parentIsLocalized || tab.localized,
version: version?.[tab.name],
})
} else {
@@ -148,6 +171,7 @@ export function countChangedFields({ comparison, config, fields, locales, versio
config,
fields: tab.fields,
locales,
parentIsLocalized: parentIsLocalized || tab.localized,
version,
})
}
@@ -176,6 +200,7 @@ type countChangedFieldsInRowsArgs = {
config: ClientConfig
field: ArrayFieldClient | BlocksFieldClient
locales: string[] | undefined
parentIsLocalized: boolean
versionRows: unknown[]
}
@@ -184,6 +209,7 @@ export function countChangedFieldsInRows({
config,
field,
locales,
parentIsLocalized,
versionRows = [],
}: countChangedFieldsInRowsArgs) {
let count = 0
@@ -207,6 +233,7 @@ export function countChangedFieldsInRows({
config,
fields: rowFields,
locales,
parentIsLocalized: parentIsLocalized || field.localized,
version: versionRow,
})

View File

@@ -229,6 +229,7 @@ export const VersionView: PayloadServerReactComponent<EditViewComponent> = async
i18n,
modifiedOnly,
parentIndexPath: '',
parentIsLocalized: false,
parentPath: '',
parentSchemaPath: '',
req,

View File

@@ -120,11 +120,12 @@ export type BaseRichTextHookArgs<
indexPath: number[]
/** The full original document in `update` operations. In the `afterChange` hook, this is the resulting document of the operation. */
originalDoc?: TData
parentIsLocalized: boolean
/**
* The path of the field, e.g. ["group", "myArray", 1, "textField"]. The path is the schemaPath but with indexes and would be used in the context of field data, not field schemas.
*/
path: (number | string)[]
/** The Express request object. It is mocked for Local API operations. */
req: PayloadRequest
/**
@@ -208,6 +209,7 @@ type RichTextAdapterBase<
findMany: boolean
flattenLocales: boolean
overrideAccess?: boolean
parentIsLocalized: boolean
populateArg?: PopulateType
populationPromises: Promise<void>[]
req: PayloadRequest

View File

@@ -61,6 +61,7 @@ export type FieldDiffClientProps<TClientField extends ClientFieldWithOptionalTyp
* If this field is localized, this will be the locale of the field
*/
locale?: string
parentIsLocalized: boolean
/**
* Field value from the current version
*/

View File

@@ -522,6 +522,11 @@ export type SanitizedJoin = {
* The path of the join field in dot notation
*/
joinPath: string
/**
* `parentIsLocalized` is true if any parent field of the
* field configuration defining the join is localized
*/
parentIsLocalized: boolean
targetField: RelationshipField | UploadField
}

View File

@@ -36,6 +36,10 @@ import { defaults } from './defaults.js'
const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig> => {
const sanitizedConfig = { ...configToSanitize }
if (configToSanitize?.compatibility?.allowLocalizedWithinLocalized) {
process.env.NEXT_PUBLIC_PAYLOAD_COMPATIBILITY_allowLocalizedWithinLocalized = 'true'
}
// default logging level will be 'error' if not provided
sanitizedConfig.loggingLevels = {
Forbidden: 'info',

View File

@@ -940,6 +940,8 @@ export type Config = {
* to `true` only if you have an existing Payload database from pre-3.0
* that you would like to maintain without migrating. This is only
* relevant for MongoDB databases.
*
* @todo Remove in v4
*/
allowLocalizedWithinLocalized: true
}

View File

@@ -1,23 +1,22 @@
// @ts-strict-ignore
import type { Where } from '../types/index.js'
import { hasWhereAccessResult } from '../auth/index.js'
/**
* Combines two queries into a single query, using an AND operator
*/
export const combineQueries = (where: Where, access: boolean | Where): Where => {
if (!where && !access) {
return {}
}
const result: Where = {
and: [],
}
const and: Where[] = where ? [where] : []
if (where) {
result.and.push(where)
}
if (hasWhereAccessResult(access)) {
result.and.push(access)
and.push(access)
}
return result
return {
and,
}
}

View File

@@ -1,8 +1,14 @@
// @ts-strict-ignore
import type { Field, FlattenedBlock, FlattenedField } from '../fields/config/types.js'
import type { Payload } from '../index.js'
import type { PathToQuery } from './queryValidation/types.js'
// @ts-strict-ignore
import {
type Field,
fieldShouldBeLocalized,
type FlattenedBlock,
type FlattenedField,
} from '../fields/config/types.js'
export function getLocalizedPaths({
collectionSlug,
fields,
@@ -10,6 +16,7 @@ export function getLocalizedPaths({
incomingPath,
locale,
overrideAccess = false,
parentIsLocalized,
payload,
}: {
collectionSlug?: string
@@ -18,6 +25,10 @@ export function getLocalizedPaths({
incomingPath: string
locale?: string
overrideAccess?: boolean
/**
* @todo make required in v4.0. Usually, you'd wanna pass this through
*/
parentIsLocalized?: boolean
payload: Payload
}): PathToQuery[] {
const pathSegments = incomingPath.split('.')
@@ -31,6 +42,7 @@ export function getLocalizedPaths({
fields,
globalSlug,
invalid: false,
parentIsLocalized,
path: '',
},
]
@@ -45,6 +57,7 @@ export function getLocalizedPaths({
let currentPath = path ? `${path}.${segment}` : segment
let fieldsToSearch: FlattenedField[]
let _parentIsLocalized = parentIsLocalized
let matchedField: FlattenedField
@@ -76,6 +89,7 @@ export function getLocalizedPaths({
} else {
fieldsToSearch = lastIncompletePath.fields
}
_parentIsLocalized = parentIsLocalized || lastIncompletePath.field?.localized
matchedField = fieldsToSearch.find((field) => field.name === segment)
}
@@ -117,7 +131,10 @@ export function getLocalizedPaths({
// Skip the next iteration, because it's a locale
i += 1
currentPath = `${currentPath}.${nextSegment}`
} else if (localizationConfig && 'localized' in matchedField && matchedField.localized) {
} else if (
localizationConfig &&
fieldShouldBeLocalized({ field: matchedField, parentIsLocalized: _parentIsLocalized })
) {
currentPath = `${currentPath}.${locale}`
}
@@ -167,6 +184,7 @@ export function getLocalizedPaths({
globalSlug,
incomingPath: nestedPathToQuery,
locale,
parentIsLocalized: false,
payload,
})

View File

@@ -17,5 +17,9 @@ export type PathToQuery = {
fields?: FlattenedField[]
globalSlug?: string
invalid?: boolean
/**
* @todo make required in v4.0
*/
parentIsLocalized: boolean
path: string
}

View File

@@ -18,6 +18,7 @@ type Args = {
globalConfig?: SanitizedGlobalConfig
operator: string
overrideAccess: boolean
parentIsLocalized?: boolean
path: string
policies: EntityPolicies
req: PayloadRequest
@@ -35,6 +36,7 @@ export async function validateSearchParam({
globalConfig,
operator,
overrideAccess,
parentIsLocalized,
path: incomingPath,
policies,
req,
@@ -68,6 +70,7 @@ export async function validateSearchParam({
incomingPath: sanitizedPath,
locale: req.locale,
overrideAccess,
parentIsLocalized,
payload: req.payload,
})
}

View File

@@ -27,6 +27,7 @@ export {
fieldIsPresentationalOnly,
fieldIsSidebar,
fieldIsVirtual,
fieldShouldBeLocalized,
fieldSupportsMany,
optionIsObject,
optionIsValue,

View File

@@ -104,7 +104,7 @@ export const sanitizeFields = async ({
}
if (field.type === 'join') {
sanitizeJoinField({ config, field, joinPath, joins })
sanitizeJoinField({ config, field, joinPath, joins, parentIsLocalized })
}
if (field.type === 'relationship' || field.type === 'upload') {
@@ -160,7 +160,12 @@ export const sanitizeFields = async ({
if (typeof field.localized !== 'undefined') {
let shouldDisableLocalized = !config.localization
if (!config.compatibility?.allowLocalizedWithinLocalized && parentIsLocalized) {
if (
process.env.NEXT_PUBLIC_PAYLOAD_COMPATIBILITY_allowLocalizedWithinLocalized !== 'true' &&
parentIsLocalized &&
// @todo PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY=true will be the default in 4.0
process.env.PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY !== 'true'
) {
shouldDisableLocalized = true
}
@@ -185,7 +190,7 @@ export const sanitizeFields = async ({
field.access = {}
}
setDefaultBeforeDuplicate(field)
setDefaultBeforeDuplicate(field, parentIsLocalized)
}
if (!field.admin) {

View File

@@ -1,21 +1,29 @@
// @ts-strict-ignore
import type { SanitizedJoin, SanitizedJoins } from '../../collections/config/types.js'
import type { Config, SanitizedConfig } from '../../config/types.js'
import type { FlattenedJoinField, JoinField, RelationshipField, UploadField } from './types.js'
import { APIError } from '../../errors/index.js'
import { InvalidFieldJoin } from '../../errors/InvalidFieldJoin.js'
import { traverseFields } from '../../utilities/traverseFields.js'
import {
fieldShouldBeLocalized,
type FlattenedJoinField,
type JoinField,
type RelationshipField,
type UploadField,
} from './types.js'
export const sanitizeJoinField = ({
config,
field,
joinPath,
joins,
parentIsLocalized,
}: {
config: Config
field: FlattenedJoinField | JoinField
joinPath?: string
joins?: SanitizedJoins
parentIsLocalized: boolean
}) => {
// the `joins` arg is not passed for globals or when recursing on fields that do not allow a join field
if (typeof joins === 'undefined') {
@@ -27,6 +35,7 @@ export const sanitizeJoinField = ({
const join: SanitizedJoin = {
field,
joinPath: `${joinPath ? joinPath + '.' : ''}${field.name}`,
parentIsLocalized,
targetField: undefined,
}
const joinCollection = config.collections.find(
@@ -43,14 +52,14 @@ export const sanitizeJoinField = ({
let localized = false
// Traverse fields and match based on the schema path
traverseFields({
callback: ({ field, next }) => {
callback: ({ field, next, parentIsLocalized }) => {
if (!('name' in field) || !field.name) {
return
}
const currentSegment = pathSegments[currentSegmentIndex]
// match field on path segments
if ('name' in field && field.name === currentSegment) {
if ('localized' in field && field.localized) {
if (fieldShouldBeLocalized({ field, parentIsLocalized })) {
localized = true
const fieldIndex = currentSegmentIndex
@@ -93,6 +102,7 @@ export const sanitizeJoinField = ({
},
config: config as unknown as SanitizedConfig,
fields: joinCollection.fields,
parentIsLocalized: false,
})
if (!joinRelationship) {

View File

@@ -1854,10 +1854,35 @@ export function tabHasName<TField extends ClientTab | Tab>(tab: TField): tab is
return 'name' in tab
}
/**
* Check if a field has localized: true set. This does not check if a field *should*
* be localized. To check if a field should be localized, use `fieldShouldBeLocalized`.
*
* @deprecated this will be removed or modified in v4.0, as `fieldIsLocalized` can easily lead to bugs due to
* parent field localization not being taken into account.
*/
export function fieldIsLocalized(field: Field | Tab): boolean {
return 'localized' in field && field.localized
}
/**
* Similar to `fieldIsLocalized`, but returns `false` if any parent field is localized.
*/
export function fieldShouldBeLocalized({
field,
parentIsLocalized,
}: {
field: ClientField | ClientTab | Field | Tab
parentIsLocalized: boolean
}): boolean {
return (
'localized' in field &&
field.localized &&
(!parentIsLocalized ||
process.env.NEXT_PUBLIC_PAYLOAD_COMPATIBILITY_allowLocalizedWithinLocalized === 'true')
)
}
export function fieldIsVirtual(field: Field | Tab): boolean {
return 'virtual' in field && field.virtual
}

View File

@@ -46,6 +46,7 @@ export const afterChange = async <T extends JsonObject>({
global,
operation,
parentIndexPath: '',
parentIsLocalized: false,
parentPath: '',
parentSchemaPath: '',
previousDoc,

View File

@@ -25,6 +25,7 @@ type Args = {
global: null | SanitizedGlobalConfig
operation: 'create' | 'update'
parentIndexPath: string
parentIsLocalized: boolean
parentPath: string
parentSchemaPath: string
previousDoc: JsonObject
@@ -49,6 +50,7 @@ export const promise = async ({
global,
operation,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
previousDoc,
@@ -123,6 +125,7 @@ export const promise = async ({
global,
operation,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath,
previousDoc,
@@ -166,6 +169,7 @@ export const promise = async ({
global,
operation,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath + '.' + block.slug,
previousDoc,
@@ -196,6 +200,7 @@ export const promise = async ({
global,
operation,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath,
parentSchemaPath: schemaPath,
previousDoc,
@@ -219,6 +224,7 @@ export const promise = async ({
global,
operation,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path,
parentSchemaPath: schemaPath,
previousDoc,
@@ -255,6 +261,7 @@ export const promise = async ({
indexPath: indexPathSegments,
operation,
originalDoc: doc,
parentIsLocalized,
path: pathSegments,
previousDoc,
previousSiblingDoc,
@@ -296,6 +303,7 @@ export const promise = async ({
global,
operation,
parentIndexPath: isNamedTab ? '' : indexPath,
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: isNamedTab ? path : parentPath,
parentSchemaPath: schemaPath,
previousDoc,
@@ -319,6 +327,7 @@ export const promise = async ({
global,
operation,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath: path,
parentSchemaPath: schemaPath,
previousDoc,

View File

@@ -20,6 +20,10 @@ type Args = {
global: null | SanitizedGlobalConfig
operation: 'create' | 'update'
parentIndexPath: string
/**
* @todo make required in v4.0
*/
parentIsLocalized?: boolean
parentPath: string
parentSchemaPath: string
previousDoc: JsonObject
@@ -40,6 +44,7 @@ export const traverseFields = async ({
global,
operation,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
previousDoc,
@@ -64,6 +69,7 @@ export const traverseFields = async ({
global,
operation,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
previousDoc,

View File

@@ -85,6 +85,7 @@ export async function afterRead<T extends JsonObject>(args: Args<T>): Promise<T>
locale,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: false,
parentPath: '',
parentSchemaPath: '',
populate,

View File

@@ -13,7 +13,7 @@ import type {
import type { Block, Field, TabAsField } from '../../config/types.js'
import { MissingEditorProp } from '../../../errors/index.js'
import { fieldAffectsData, tabHasName } from '../../config/types.js'
import { fieldAffectsData, fieldShouldBeLocalized, tabHasName } from '../../config/types.js'
import { getDefaultValue } from '../../getDefaultValue.js'
import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js'
import { relationshipPopulationPromise } from './relationshipPopulationPromise.js'
@@ -43,6 +43,10 @@ type Args = {
locale: null | string
overrideAccess: boolean
parentIndexPath: string
/**
* @todo make required in v4.0
*/
parentIsLocalized?: boolean
parentPath: string
parentSchemaPath: string
populate?: PopulateType
@@ -83,6 +87,7 @@ export const promise = async ({
locale,
overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
populate,
@@ -139,7 +144,7 @@ export const promise = async ({
fieldAffectsData(field) &&
typeof siblingDoc[field.name] === 'object' &&
siblingDoc[field.name] !== null &&
field.localized &&
fieldShouldBeLocalized({ field, parentIsLocalized }) &&
locale !== 'all' &&
req.payload.config.localization
@@ -236,7 +241,7 @@ export const promise = async ({
await priorHook
const shouldRunHookOnAllLocales =
field.localized &&
fieldShouldBeLocalized({ field, parentIsLocalized }) &&
(locale === 'all' || !flattenLocales) &&
typeof siblingDoc[field.name] === 'object'
@@ -352,6 +357,7 @@ export const promise = async ({
field,
locale,
overrideAccess,
parentIsLocalized,
populate,
req,
showHiddenFields,
@@ -393,6 +399,7 @@ export const promise = async ({
locale,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath,
populate,
@@ -427,6 +434,7 @@ export const promise = async ({
locale,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath,
populate,
@@ -511,6 +519,7 @@ export const promise = async ({
locale,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath + '.' + block.slug,
populate,
@@ -555,6 +564,7 @@ export const promise = async ({
locale,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath + '.' + block.slug,
populate,
@@ -595,6 +605,7 @@ export const promise = async ({
locale,
overrideAccess,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath,
parentSchemaPath: schemaPath,
populate,
@@ -637,6 +648,7 @@ export const promise = async ({
locale,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path,
parentSchemaPath: schemaPath,
populate,
@@ -669,7 +681,7 @@ export const promise = async ({
await priorHook
const shouldRunHookOnAllLocales =
field.localized &&
fieldShouldBeLocalized({ field, parentIsLocalized }) &&
(locale === 'all' || !flattenLocales) &&
typeof siblingDoc[field.name] === 'object'
@@ -694,6 +706,7 @@ export const promise = async ({
operation: 'read',
originalDoc: doc,
overrideAccess,
parentIsLocalized,
path: pathSegments,
populate,
populationPromises,
@@ -732,6 +745,7 @@ export const promise = async ({
operation: 'read',
originalDoc: doc,
overrideAccess,
parentIsLocalized,
path: pathSegments,
populate,
populationPromises,
@@ -790,6 +804,7 @@ export const promise = async ({
locale,
overrideAccess,
parentIndexPath: isNamedTab ? '' : indexPath,
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: isNamedTab ? path : parentPath,
parentSchemaPath: schemaPath,
populate,
@@ -824,6 +839,7 @@ export const promise = async ({
locale,
overrideAccess,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath: path,
parentSchemaPath: schemaPath,
populate,

View File

@@ -3,7 +3,7 @@ import type { PayloadRequest, PopulateType } from '../../../types/index.js'
import type { JoinField, RelationshipField, UploadField } from '../../config/types.js'
import { createDataloaderCacheKey } from '../../../collections/dataloader.js'
import { fieldHasMaxDepth, fieldSupportsMany } from '../../config/types.js'
import { fieldHasMaxDepth, fieldShouldBeLocalized, fieldSupportsMany } from '../../config/types.js'
type PopulateArgs = {
currentDepth: number
@@ -115,6 +115,7 @@ type PromiseArgs = {
field: JoinField | RelationshipField | UploadField
locale: null | string
overrideAccess: boolean
parentIsLocalized: boolean
populate?: PopulateType
req: PayloadRequest
showHiddenFields: boolean
@@ -129,6 +130,7 @@ export const relationshipPopulationPromise = async ({
field,
locale,
overrideAccess,
parentIsLocalized,
populate: populateArg,
req,
showHiddenFields,
@@ -140,7 +142,7 @@ export const relationshipPopulationPromise = async ({
if (field.type === 'join' || (fieldSupportsMany(field) && field.hasMany)) {
if (
field.localized &&
fieldShouldBeLocalized({ field, parentIsLocalized }) &&
locale === 'all' &&
typeof siblingDoc[field.name] === 'object' &&
siblingDoc[field.name] !== null

View File

@@ -35,6 +35,10 @@ type Args = {
locale: null | string
overrideAccess: boolean
parentIndexPath: string
/**
* @todo make required in v4.0
*/
parentIsLocalized?: boolean
parentPath: string
parentSchemaPath: string
populate?: PopulateType
@@ -65,6 +69,7 @@ export const traverseFields = ({
locale,
overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
populate,
@@ -97,6 +102,7 @@ export const traverseFields = ({
locale,
overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
populate,

View File

@@ -59,6 +59,7 @@ export const beforeChange = async <T extends JsonObject>({
mergeLocaleActions,
operation,
parentIndexPath: '',
parentIsLocalized: false,
parentPath: '',
parentSchemaPath: '',
req,

View File

@@ -11,7 +11,7 @@ import { MissingEditorProp } from '../../../errors/index.js'
import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js'
import { getLabelFromPath } from '../../../utilities/getLabelFromPath.js'
import { getTranslatedLabel } from '../../../utilities/getTranslatedLabel.js'
import { fieldAffectsData, tabHasName } from '../../config/types.js'
import { fieldAffectsData, fieldShouldBeLocalized, tabHasName } from '../../config/types.js'
import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js'
import { getExistingRowDoc } from './getExistingRowDoc.js'
import { traverseFields } from './traverseFields.js'
@@ -34,6 +34,7 @@ type Args = {
mergeLocaleActions: (() => Promise<void>)[]
operation: Operation
parentIndexPath: string
parentIsLocalized: boolean
parentPath: string
parentSchemaPath: string
req: PayloadRequest
@@ -67,6 +68,7 @@ export const promise = async ({
mergeLocaleActions,
operation,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
req,
@@ -98,7 +100,7 @@ export const promise = async ({
if (fieldAffectsData(field)) {
// skip validation if the field is localized and the incoming data is null
if (field.localized && operationLocale !== defaultLocale) {
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && operationLocale !== defaultLocale) {
if (['array', 'blocks'].includes(field.type) && siblingData[field.name] === null) {
skipValidationFromHere = true
}
@@ -189,7 +191,7 @@ export const promise = async ({
}
// Push merge locale action if applicable
if (localization && field.localized) {
if (localization && fieldShouldBeLocalized({ field, parentIsLocalized })) {
mergeLocaleActions.push(async () => {
const localeData = await localization.localeCodes.reduce(
async (localizedValuesPromise: Promise<JsonObject>, locale) => {
@@ -244,6 +246,7 @@ export const promise = async ({
mergeLocaleActions,
operation,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath,
req,
@@ -301,6 +304,7 @@ export const promise = async ({
mergeLocaleActions,
operation,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath + '.' + block.slug,
req,
@@ -335,6 +339,7 @@ export const promise = async ({
mergeLocaleActions,
operation,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath,
parentSchemaPath: schemaPath,
req,
@@ -374,6 +379,7 @@ export const promise = async ({
mergeLocaleActions,
operation,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path,
parentSchemaPath: schemaPath,
req,
@@ -432,6 +438,7 @@ export const promise = async ({
mergeLocaleActions,
operation,
originalDoc: doc,
parentIsLocalized,
path: pathSegments,
previousSiblingDoc: siblingDoc,
previousValue: siblingDoc[field.name],
@@ -491,6 +498,7 @@ export const promise = async ({
mergeLocaleActions,
operation,
parentIndexPath: isNamedTab ? '' : indexPath,
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: isNamedTab ? path : parentPath,
parentSchemaPath: schemaPath,
req,
@@ -518,6 +526,7 @@ export const promise = async ({
mergeLocaleActions,
operation,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath: path,
parentSchemaPath: schemaPath,
req,

View File

@@ -31,6 +31,10 @@ type Args = {
mergeLocaleActions: (() => Promise<void>)[]
operation: Operation
parentIndexPath: string
/**
* @todo make required in v4.0
*/
parentIsLocalized?: boolean
parentPath: string
parentSchemaPath: string
req: PayloadRequest
@@ -68,6 +72,7 @@ export const traverseFields = async ({
mergeLocaleActions,
operation,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
req,
@@ -95,6 +100,7 @@ export const traverseFields = async ({
mergeLocaleActions,
operation,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
req,

View File

@@ -36,6 +36,7 @@ export const beforeDuplicate = async <T extends JsonObject>({
fields: collection?.fields,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: false,
parentPath: '',
parentSchemaPath: '',
req,

View File

@@ -4,7 +4,7 @@ import type { RequestContext } from '../../../index.js'
import type { JsonObject, PayloadRequest } from '../../../types/index.js'
import type { Block, Field, FieldHookArgs, TabAsField } from '../../config/types.js'
import { fieldAffectsData } from '../../config/types.js'
import { fieldAffectsData, fieldShouldBeLocalized } from '../../config/types.js'
import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js'
import { runBeforeDuplicateHooks } from './runHook.js'
import { traverseFields } from './traverseFields.js'
@@ -22,6 +22,7 @@ type Args<T> = {
id?: number | string
overrideAccess: boolean
parentIndexPath: string
parentIsLocalized: boolean
parentPath: string
parentSchemaPath: string
req: PayloadRequest
@@ -39,6 +40,7 @@ export const promise = async <T>({
fieldIndex,
overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
req,
@@ -61,7 +63,7 @@ export const promise = async <T>({
if (fieldAffectsData(field)) {
let fieldData = siblingDoc?.[field.name]
const fieldIsLocalized = field.localized && localization
const fieldIsLocalized = localization && fieldShouldBeLocalized({ field, parentIsLocalized })
// Run field beforeDuplicate hooks
if (Array.isArray(field.hooks?.beforeDuplicate)) {
@@ -162,6 +164,7 @@ export const promise = async <T>({
fields: field.fields,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath,
req,
@@ -200,6 +203,7 @@ export const promise = async <T>({
fields: block.fields,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath + '.' + block.slug,
req,
@@ -223,6 +227,7 @@ export const promise = async <T>({
fields: field.fields,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path,
parentSchemaPath: schemaPath,
req,
@@ -259,6 +264,7 @@ export const promise = async <T>({
fields: field.fields,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath,
req,
@@ -301,6 +307,7 @@ export const promise = async <T>({
fields: block.fields,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath + '.' + block.slug,
req,
@@ -332,6 +339,7 @@ export const promise = async <T>({
fields: field.fields,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path,
parentSchemaPath: schemaPath,
req,
@@ -357,6 +365,7 @@ export const promise = async <T>({
fields: field.fields,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path,
parentSchemaPath: schemaPath,
req,
@@ -381,6 +390,7 @@ export const promise = async <T>({
fields: field.fields,
overrideAccess,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath,
parentSchemaPath: schemaPath,
req,
@@ -403,6 +413,7 @@ export const promise = async <T>({
fields: field.fields,
overrideAccess,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath,
parentSchemaPath: schemaPath,
req,
@@ -422,6 +433,7 @@ export const promise = async <T>({
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
overrideAccess,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath: path,
parentSchemaPath: schemaPath,
req,

View File

@@ -18,6 +18,7 @@ type Args<T> = {
id?: number | string
overrideAccess: boolean
parentIndexPath: string
parentIsLocalized: boolean
parentPath: string
parentSchemaPath: string
req: PayloadRequest
@@ -33,6 +34,7 @@ export const traverseFields = async <T>({
fields,
overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
req,
@@ -52,6 +54,7 @@ export const traverseFields = async <T>({
fieldIndex,
overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
req,

View File

@@ -49,6 +49,7 @@ export const beforeValidate = async <T extends JsonObject>({
operation,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: false,
parentPath: '',
parentSchemaPath: '',
req,

View File

@@ -33,6 +33,7 @@ type Args<T> = {
operation: 'create' | 'update'
overrideAccess: boolean
parentIndexPath: string
parentIsLocalized: boolean
parentPath: string
parentSchemaPath: string
req: PayloadRequest
@@ -64,6 +65,7 @@ export const promise = async <T>({
operation,
overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
req,
@@ -355,6 +357,7 @@ export const promise = async <T>({
operation,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath,
req,
@@ -401,6 +404,7 @@ export const promise = async <T>({
operation,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
parentSchemaPath: schemaPath + '.' + block.slug,
req,
@@ -431,6 +435,7 @@ export const promise = async <T>({
operation,
overrideAccess,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath,
parentSchemaPath: schemaPath,
req,
@@ -465,6 +470,7 @@ export const promise = async <T>({
operation,
overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path,
parentSchemaPath: schemaPath,
req,
@@ -500,6 +506,7 @@ export const promise = async <T>({
operation,
originalDoc: doc,
overrideAccess,
parentIsLocalized,
path: pathSegments,
previousSiblingDoc: siblingDoc,
previousValue: siblingData[field.name],
@@ -551,6 +558,7 @@ export const promise = async <T>({
operation,
overrideAccess,
parentIndexPath: isNamedTab ? '' : indexPath,
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: isNamedTab ? path : parentPath,
parentSchemaPath: schemaPath,
req,
@@ -574,6 +582,7 @@ export const promise = async <T>({
operation,
overrideAccess,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath: path,
parentSchemaPath: schemaPath,
req,

View File

@@ -25,6 +25,10 @@ type Args<T> = {
operation: 'create' | 'update'
overrideAccess: boolean
parentIndexPath: string
/**
* @todo make required in v4.0
*/
parentIsLocalized?: boolean
parentPath: string
parentSchemaPath: string
req: PayloadRequest
@@ -47,6 +51,7 @@ export const traverseFields = async <T>({
operation,
overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
req,
@@ -70,6 +75,7 @@ export const traverseFields = async <T>({
operation,
overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
parentSchemaPath,
req,

View File

@@ -1,6 +1,6 @@
// @ts-strict-ignore
// default beforeDuplicate hook for required and unique fields
import type { FieldAffectingData, FieldHook } from './config/types.js'
import { type FieldAffectingData, type FieldHook, fieldShouldBeLocalized } from './config/types.js'
const unique: FieldHook = ({ value }) => (typeof value === 'string' ? `${value} - Copy` : undefined)
const localizedUnique: FieldHook = ({ req, value }) =>
@@ -9,16 +9,25 @@ const uniqueRequired: FieldHook = ({ value }) => `${value} - Copy`
const localizedUniqueRequired: FieldHook = ({ req, value }) =>
`${value} - ${req?.t('general:copy') ?? 'Copy'}`
export const setDefaultBeforeDuplicate = (field: FieldAffectingData) => {
export const setDefaultBeforeDuplicate = (
field: FieldAffectingData,
parentIsLocalized: boolean,
) => {
if (
(('required' in field && field.required) || field.unique) &&
(!field.hooks?.beforeDuplicate ||
(Array.isArray(field.hooks.beforeDuplicate) && field.hooks.beforeDuplicate.length === 0))
) {
if ((field.type === 'text' || field.type === 'textarea') && field.required && field.unique) {
field.hooks.beforeDuplicate = [field.localized ? localizedUniqueRequired : uniqueRequired]
field.hooks.beforeDuplicate = [
fieldShouldBeLocalized({ field, parentIsLocalized })
? localizedUniqueRequired
: uniqueRequired,
]
} else if (field.unique) {
field.hooks.beforeDuplicate = [field.localized ? localizedUnique : unique]
field.hooks.beforeDuplicate = [
fieldShouldBeLocalized({ field, parentIsLocalized }) ? localizedUnique : unique,
]
}
}
}

View File

@@ -616,7 +616,12 @@ export class BasePayload {
}
}
traverseFields({ callback: findCustomID, config: this.config, fields: collection.fields })
traverseFields({
callback: findCustomID,
config: this.config,
fields: collection.fields,
parentIsLocalized: false,
})
this.collections[collection.slug] = {
config: collection,

View File

@@ -1,10 +1,10 @@
// @ts-strict-ignore
import type { CollectionPermission, GlobalPermission } from '../auth/types.js'
import type { CollectionPermission, FieldsPermissions, GlobalPermission } from '../auth/types.js'
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
import type { Access } from '../config/types.js'
import type { Field, FieldAccess } from '../fields/config/types.js'
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
import type { AllOperations, Document, PayloadRequest, Where } from '../types/index.js'
import type { AllOperations, JsonObject, Payload, PayloadRequest, Where } from '../types/index.js'
import { combineQueries } from '../database/combineQueries.js'
import { tabHasName } from '../fields/config/types.js'
@@ -26,11 +26,14 @@ type CreateAccessPromise = (args: {
accessLevel: 'entity' | 'field'
disableWhere?: boolean
operation: AllOperations
policiesObj: {
[key: string]: any
}
policiesObj: CollectionPermission | GlobalPermission
}) => Promise<void>
type EntityDoc = JsonObject | TypeWithID
/**
* Build up permissions object for an entity (collection or global)
*/
export async function getEntityPolicies<T extends Args>(args: T): Promise<ReturnType<T>> {
const { id, type, entity, operations, req } = args
const { data, locale, payload, user } = req
@@ -40,50 +43,51 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
fields: {},
} as ReturnType<T>
let docBeingAccessed
let docBeingAccessed: EntityDoc | Promise<EntityDoc | undefined> | undefined
async function getEntityDoc({ where }: { where?: Where } = {}): Promise<Document & TypeWithID> {
if (entity.slug) {
if (type === 'global') {
return payload.findGlobal({
slug: entity.slug,
fallbackLocale: null,
locale,
overrideAccess: true,
req,
})
}
async function getEntityDoc({ where }: { where?: Where } = {}): Promise<EntityDoc | undefined> {
if (!entity.slug) {
return undefined
}
if (type === 'collection' && id) {
if (typeof where === 'object') {
const paginatedRes = await payload.find({
collection: entity.slug,
depth: 0,
fallbackLocale: null,
limit: 1,
locale,
overrideAccess: true,
pagination: false,
req,
where: combineQueries(where, { id: { equals: id } }),
})
if (type === 'global') {
return payload.findGlobal({
slug: entity.slug,
depth: 0,
fallbackLocale: null,
locale,
overrideAccess: true,
req,
})
}
return paginatedRes?.docs?.[0] || undefined
}
return payload.findByID({
id,
if (type === 'collection' && id) {
if (typeof where === 'object') {
const paginatedRes = await payload.find({
collection: entity.slug,
depth: 0,
fallbackLocale: null,
limit: 1,
locale,
overrideAccess: true,
pagination: false,
req,
where: combineQueries(where, { id: { equals: id } }),
})
}
}
return undefined
return paginatedRes?.docs?.[0] || undefined
}
return payload.findByID({
id,
collection: entity.slug,
depth: 0,
fallbackLocale: null,
locale,
overrideAccess: true,
req,
})
}
}
const createAccessPromise: CreateAccessPromise = async ({
@@ -91,10 +95,8 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
accessLevel,
disableWhere = false,
operation,
policiesObj,
policiesObj: mutablePolicies,
}) => {
const mutablePolicies = policiesObj
if (accessLevel === 'field' && docBeingAccessed === undefined) {
// assign docBeingAccessed first as the promise to avoid multiple calls to getEntityDoc
docBeingAccessed = getEntityDoc().then((doc) => {
@@ -107,6 +109,7 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
// https://payloadcms.slack.com/archives/C048Z9C2BEX/p1702054928343769
const accessResult = await access({ id, data, doc: docBeingAccessed, req })
// Where query was returned from access function => check if document is returned when querying with where
if (typeof accessResult === 'object' && !disableWhere) {
mutablePolicies[operation] = {
permission:
@@ -120,137 +123,9 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
}
}
const executeFieldPolicies = async ({
entityPermission,
fields,
operation,
policiesObj,
}: {
entityPermission
fields: Field[]
operation: AllOperations
policiesObj
}) => {
const mutablePolicies = policiesObj.fields
// Fields don't have all operations of a collection
if (operation === 'delete' || operation === 'readVersions' || operation === 'unlock') {
return
}
await Promise.all(
fields.map(async (field) => {
if ('name' in field && field.name) {
if (!mutablePolicies[field.name]) {
mutablePolicies[field.name] = {}
}
if ('access' in field && field.access && typeof field.access[operation] === 'function') {
await createAccessPromise({
access: field.access[operation],
accessLevel: 'field',
disableWhere: true,
operation,
policiesObj: mutablePolicies[field.name],
})
} else {
mutablePolicies[field.name][operation] = {
permission: policiesObj[operation]?.permission,
}
}
if ('fields' in field && field.fields) {
if (!mutablePolicies[field.name].fields) {
mutablePolicies[field.name].fields = {}
}
await executeFieldPolicies({
entityPermission,
fields: field.fields,
operation,
policiesObj: mutablePolicies[field.name],
})
}
if (
('blocks' in field && field.blocks) ||
('blockReferences' in field && field.blockReferences)
) {
if (!mutablePolicies[field.name]?.blocks) {
mutablePolicies[field.name].blocks = {}
}
await Promise.all(
(field.blockReferences ?? field.blocks).map(async (_block) => {
const block = typeof _block === 'string' ? payload.blocks[_block] : _block // TODO: Skip over string blocks
if (!mutablePolicies[field.name].blocks?.[block.slug]) {
mutablePolicies[field.name].blocks[block.slug] = {
fields: {},
[operation]: { permission: entityPermission },
}
} else if (!mutablePolicies[field.name].blocks[block.slug][operation]) {
mutablePolicies[field.name].blocks[block.slug][operation] = {
permission: entityPermission,
}
}
await executeFieldPolicies({
entityPermission,
fields: block.fields,
operation,
policiesObj: mutablePolicies[field.name].blocks[block.slug],
})
}),
)
}
} else if ('fields' in field && field.fields) {
await executeFieldPolicies({
entityPermission,
fields: field.fields,
operation,
policiesObj,
})
} else if (field.type === 'tabs') {
await Promise.all(
field.tabs.map(async (tab) => {
if (tabHasName(tab)) {
if (!mutablePolicies[tab.name]) {
mutablePolicies[tab.name] = {
fields: {},
[operation]: { permission: entityPermission },
}
} else if (!mutablePolicies[tab.name][operation]) {
mutablePolicies[tab.name][operation] = { permission: entityPermission }
}
await executeFieldPolicies({
entityPermission,
fields: tab.fields,
operation,
policiesObj: mutablePolicies[tab.name],
})
} else {
await executeFieldPolicies({
entityPermission,
fields: tab.fields,
operation,
policiesObj,
})
}
}),
)
}
}),
)
}
await operations.reduce(async (priorOperation, operation) => {
await priorOperation
let entityAccessPromise: Promise<void>
for (const operation of operations) {
if (typeof entity.access[operation] === 'function') {
entityAccessPromise = createAccessPromise({
await createAccessPromise({
access: entity.access[operation],
accessLevel: 'entity',
operation,
@@ -262,15 +137,156 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
}
}
await entityAccessPromise
await executeFieldPolicies({
entityPermission: policies[operation].permission,
createAccessPromise,
entityPermission: policies[operation].permission as boolean,
fields: entity.fields,
operation,
payload,
policiesObj: policies,
})
}, Promise.resolve())
}
return policies
}
/**
* Build up permissions object and run access functions for each field of an entity
*/
const executeFieldPolicies = async ({
createAccessPromise,
entityPermission,
fields,
operation,
payload,
policiesObj,
}: {
createAccessPromise: CreateAccessPromise
entityPermission: boolean
fields: Field[]
operation: AllOperations
payload: Payload
policiesObj: CollectionPermission | FieldsPermissions | GlobalPermission
}) => {
const mutablePolicies = policiesObj.fields
// Fields don't have all operations of a collection
if (operation === 'delete' || operation === 'readVersions' || operation === 'unlock') {
return
}
await Promise.all(
fields.map(async (field) => {
if ('name' in field && field.name) {
if (!mutablePolicies[field.name]) {
mutablePolicies[field.name] = {}
}
if ('access' in field && field.access && typeof field.access[operation] === 'function') {
await createAccessPromise({
access: field.access[operation],
accessLevel: 'field',
disableWhere: true,
operation,
policiesObj: mutablePolicies[field.name],
})
} else {
mutablePolicies[field.name][operation] = {
permission: policiesObj[operation]?.permission,
}
}
if ('fields' in field && field.fields) {
if (!mutablePolicies[field.name].fields) {
mutablePolicies[field.name].fields = {}
}
await executeFieldPolicies({
createAccessPromise,
entityPermission,
fields: field.fields,
operation,
payload,
policiesObj: mutablePolicies[field.name],
})
}
if (
('blocks' in field && field.blocks?.length) ||
('blockReferences' in field && field.blockReferences?.length)
) {
if (!mutablePolicies[field.name]?.blocks) {
mutablePolicies[field.name].blocks = {}
}
await Promise.all(
(field.blockReferences ?? field.blocks).map(async (_block) => {
const block = typeof _block === 'string' ? payload.blocks[_block] : _block // TODO: Skip over string blocks
if (!mutablePolicies[field.name].blocks?.[block.slug]) {
mutablePolicies[field.name].blocks[block.slug] = {
fields: {},
[operation]: { permission: entityPermission },
}
} else if (!mutablePolicies[field.name].blocks[block.slug][operation]) {
mutablePolicies[field.name].blocks[block.slug][operation] = {
permission: entityPermission,
}
}
await executeFieldPolicies({
createAccessPromise,
entityPermission,
fields: block.fields,
operation,
payload,
policiesObj: mutablePolicies[field.name].blocks[block.slug],
})
}),
)
}
} else if ('fields' in field && field.fields) {
await executeFieldPolicies({
createAccessPromise,
entityPermission,
fields: field.fields,
operation,
payload,
policiesObj,
})
} else if (field.type === 'tabs') {
await Promise.all(
field.tabs.map(async (tab) => {
if (tabHasName(tab)) {
if (!mutablePolicies[tab.name]) {
mutablePolicies[tab.name] = {
fields: {},
[operation]: { permission: entityPermission },
}
} else if (!mutablePolicies[tab.name][operation]) {
mutablePolicies[tab.name][operation] = { permission: entityPermission }
}
await executeFieldPolicies({
createAccessPromise,
entityPermission,
fields: tab.fields,
operation,
payload,
policiesObj: mutablePolicies[tab.name],
})
} else {
await executeFieldPolicies({
createAccessPromise,
entityPermission,
fields: tab.fields,
operation,
payload,
policiesObj,
})
}
}),
)
}
}),
)
}

View File

@@ -2,7 +2,7 @@
import type { Config, SanitizedConfig } from '../config/types.js'
import type { ArrayField, Block, BlocksField, Field, TabAsField } from '../fields/config/types.js'
import { fieldHasSubFields } from '../fields/config/types.js'
import { fieldHasSubFields, fieldShouldBeLocalized } from '../fields/config/types.js'
const traverseArrayOrBlocksField = ({
callback,
@@ -12,6 +12,7 @@ const traverseArrayOrBlocksField = ({
field,
fillEmpty,
leavesFirst,
parentIsLocalized,
parentRef,
}: {
callback: TraverseFieldsCallback
@@ -21,6 +22,7 @@ const traverseArrayOrBlocksField = ({
field: ArrayField | BlocksField
fillEmpty: boolean
leavesFirst: boolean
parentIsLocalized: boolean
parentRef?: unknown
}) => {
if (fillEmpty) {
@@ -32,6 +34,7 @@ const traverseArrayOrBlocksField = ({
fields: field.fields,
isTopLevel: false,
leavesFirst,
parentIsLocalized: parentIsLocalized || field.localized,
parentRef,
})
}
@@ -48,6 +51,7 @@ const traverseArrayOrBlocksField = ({
fields: block.fields,
isTopLevel: false,
leavesFirst,
parentIsLocalized: parentIsLocalized || field.localized,
parentRef,
})
}
@@ -80,6 +84,7 @@ const traverseArrayOrBlocksField = ({
fillEmpty,
isTopLevel: false,
leavesFirst,
parentIsLocalized: parentIsLocalized || field.localized,
parentRef,
ref,
})
@@ -96,6 +101,7 @@ export type TraverseFieldsCallback = (args: {
* Function that when called will skip the current field and continue to the next
*/
next?: () => void
parentIsLocalized: boolean
/**
* The parent reference object
*/
@@ -120,6 +126,7 @@ type TraverseFieldsArgs = {
* The return value of the callback function will be ignored.
*/
leavesFirst?: boolean
parentIsLocalized?: boolean
parentRef?: Record<string, unknown> | unknown
ref?: Record<string, unknown> | unknown
}
@@ -141,6 +148,7 @@ export const traverseFields = ({
fillEmpty = true,
isTopLevel = true,
leavesFirst = false,
parentIsLocalized,
parentRef = {},
ref = {},
}: TraverseFieldsArgs): void => {
@@ -158,10 +166,10 @@ export const traverseFields = ({
return
}
if (!leavesFirst && callback && callback({ field, next, parentRef, ref })) {
if (!leavesFirst && callback && callback({ field, next, parentIsLocalized, parentRef, ref })) {
return true
} else if (leavesFirst) {
callbackStack.push(() => callback({ field, next, parentRef, ref }))
callbackStack.push(() => callback({ field, next, parentIsLocalized, parentRef, ref }))
}
if (skip) {
@@ -199,6 +207,7 @@ export const traverseFields = ({
callback({
field: { ...tab, type: 'tab' },
next,
parentIsLocalized,
parentRef: currentParentRef,
ref: tabRef,
})
@@ -209,6 +218,7 @@ export const traverseFields = ({
callback({
field: { ...tab, type: 'tab' },
next,
parentIsLocalized,
parentRef: currentParentRef,
ref: tabRef,
}),
@@ -228,6 +238,7 @@ export const traverseFields = ({
fillEmpty,
isTopLevel: false,
leavesFirst,
parentIsLocalized: true,
parentRef: currentParentRef,
ref: tabRef[key],
})
@@ -241,6 +252,7 @@ export const traverseFields = ({
callback({
field: { ...tab, type: 'tab' },
next,
parentIsLocalized,
parentRef: currentParentRef,
ref: tabRef,
})
@@ -251,6 +263,7 @@ export const traverseFields = ({
callback({
field: { ...tab, type: 'tab' },
next,
parentIsLocalized,
parentRef: currentParentRef,
ref: tabRef,
}),
@@ -267,6 +280,7 @@ export const traverseFields = ({
fillEmpty,
isTopLevel: false,
leavesFirst,
parentIsLocalized: false,
parentRef: currentParentRef,
ref: tabRef,
})
@@ -286,7 +300,7 @@ export const traverseFields = ({
if (!ref[field.name]) {
if (fillEmpty) {
if (field.type === 'group') {
if (field.localized) {
if (fieldShouldBeLocalized({ field, parentIsLocalized })) {
ref[field.name] = {
en: {},
}
@@ -294,7 +308,7 @@ export const traverseFields = ({
ref[field.name] = {}
}
} else if (field.type === 'array' || field.type === 'blocks') {
if (field.localized) {
if (fieldShouldBeLocalized({ field, parentIsLocalized })) {
ref[field.name] = {
en: [],
}
@@ -311,7 +325,7 @@ export const traverseFields = ({
if (
field.type === 'group' &&
field.localized &&
fieldShouldBeLocalized({ field, parentIsLocalized }) &&
currentRef &&
typeof currentRef === 'object'
) {
@@ -325,6 +339,7 @@ export const traverseFields = ({
fillEmpty,
isTopLevel: false,
leavesFirst,
parentIsLocalized: true,
parentRef: currentParentRef,
ref: currentRef[key],
})
@@ -338,7 +353,7 @@ export const traverseFields = ({
currentRef &&
typeof currentRef === 'object'
) {
if (field.localized) {
if (fieldShouldBeLocalized({ field, parentIsLocalized })) {
if (Array.isArray(currentRef)) {
return
}
@@ -357,6 +372,7 @@ export const traverseFields = ({
field,
fillEmpty,
leavesFirst,
parentIsLocalized: true,
parentRef: currentParentRef,
})
}
@@ -369,6 +385,7 @@ export const traverseFields = ({
field,
fillEmpty,
leavesFirst,
parentIsLocalized,
parentRef: currentParentRef,
})
}
@@ -381,6 +398,7 @@ export const traverseFields = ({
fillEmpty,
isTopLevel: false,
leavesFirst,
parentIsLocalized,
parentRef: currentParentRef,
ref: currentRef,
})

View File

@@ -17,11 +17,13 @@ export const blockPopulationPromiseHOC = (
depth,
draft,
editorPopulationPromises,
field,
fieldPromises,
findMany,
flattenLocales,
node,
overrideAccess,
parentIsLocalized,
populationPromises,
req,
showHiddenFields,
@@ -46,6 +48,7 @@ export const blockPopulationPromiseHOC = (
findMany,
flattenLocales,
overrideAccess,
parentIsLocalized: parentIsLocalized || field.localized || false,
populationPromises,
req,
showHiddenFields,

View File

@@ -13,11 +13,13 @@ export const linkPopulationPromiseHOC = (
depth,
draft,
editorPopulationPromises,
field,
fieldPromises,
findMany,
flattenLocales,
node,
overrideAccess,
parentIsLocalized,
populationPromises,
req,
showHiddenFields,
@@ -42,6 +44,7 @@ export const linkPopulationPromiseHOC = (
findMany,
flattenLocales,
overrideAccess,
parentIsLocalized: parentIsLocalized || field.localized || false,
populationPromises,
req,
showHiddenFields,

View File

@@ -30,23 +30,7 @@ import type { AdapterProps } from '../types.js'
import type { HTMLConverter } from './converters/html/converter/types.js'
import type { BaseClientFeatureProps } from './typesClient.js'
export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexicalNode> = ({
context,
currentDepth,
depth,
draft,
editorPopulationPromises,
field,
fieldPromises,
findMany,
flattenLocales,
node,
overrideAccess,
populationPromises,
req,
showHiddenFields,
siblingDoc,
}: {
export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexicalNode> = (args: {
context: RequestContext
currentDepth: number
depth: number
@@ -64,6 +48,7 @@ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexica
flattenLocales: boolean
node: T
overrideAccess: boolean
parentIsLocalized: boolean
populationPromises: Promise<void>[]
req: PayloadRequest
showHiddenFields: boolean

View File

@@ -14,11 +14,13 @@ export const uploadPopulationPromiseHOC = (
depth,
draft,
editorPopulationPromises,
field,
fieldPromises,
findMany,
flattenLocales,
node,
overrideAccess,
parentIsLocalized,
populationPromises,
req,
showHiddenFields,
@@ -59,6 +61,8 @@ export const uploadPopulationPromiseHOC = (
currentDepth,
data: node.fields || {},
depth,
parentIsLocalized: parentIsLocalized || field.localized || false,
draft,
editorPopulationPromises,
fieldPromises,

View File

@@ -157,6 +157,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
findMany,
flattenLocales,
overrideAccess,
parentIsLocalized,
populationPromises,
req,
showHiddenFields,
@@ -175,6 +176,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
findMany,
flattenLocales,
overrideAccess,
parentIsLocalized,
populationPromises,
req,
showHiddenFields,
@@ -189,10 +191,12 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
collection,
context: _context,
data,
field,
global,
indexPath,
operation,
originalDoc,
parentIsLocalized,
path,
previousDoc,
previousValue,
@@ -268,7 +272,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
originalNode: originalNodeIDMap[id],
parentRichTextFieldPath: path,
parentRichTextFieldSchemaPath: schemaPath,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
previousNode: previousNodeIDMap[id]!,
req,
})
@@ -282,10 +286,9 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
if (subFieldFn && subFieldDataFn) {
const subFields = subFieldFn({ node, req })
const nodeSiblingData = subFieldDataFn({ node, req }) ?? {}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const nodeSiblingDoc = subFieldDataFn({ node: originalNodeIDMap[id]!, req }) ?? {}
const nodePreviousSiblingDoc =
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
subFieldDataFn({ node: previousNodeIDMap[id]!, req }) ?? {}
if (subFields?.length) {
@@ -299,6 +302,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
global,
operation,
parentIndexPath: indexPath.join('-'),
parentIsLocalized: parentIsLocalized || field.localized || false,
parentPath: path.join('.'),
parentSchemaPath: schemaPath.join('.'),
previousDoc,
@@ -325,6 +329,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
depth,
draft,
fallbackLocale,
field,
fieldPromises,
findMany,
flattenLocales,
@@ -333,6 +338,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
locale,
originalDoc,
overrideAccess,
parentIsLocalized,
path,
populate,
populationPromises,
@@ -419,6 +425,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
locale: locale!,
overrideAccess: overrideAccess!,
parentIndexPath: indexPath.join('-'),
parentIsLocalized: parentIsLocalized || field.localized || false,
parentPath: path.join('.'),
parentSchemaPath: schemaPath.join('.'),
populate,
@@ -450,6 +457,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
mergeLocaleActions,
operation,
originalDoc,
parentIsLocalized,
path,
previousValue,
req,
@@ -543,7 +551,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
originalNodeWithLocales: originalNodeWithLocalesIDMap[id],
parentRichTextFieldPath: path,
parentRichTextFieldSchemaPath: schemaPath,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
previousNode: previousNodeIDMap[id]!,
req,
skipValidation: skipValidation!,
@@ -561,12 +569,10 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
const nodeSiblingData = subFieldDataFn({ node, req }) ?? {}
const nodeSiblingDocWithLocales =
subFieldDataFn({
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
node: originalNodeWithLocalesIDMap[id]!,
req,
}) ?? {}
const nodePreviousSiblingDoc =
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
subFieldDataFn({ node: previousNodeIDMap[id]!, req }) ?? {}
if (subFields?.length) {
@@ -584,6 +590,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
mergeLocaleActions: mergeLocaleActions!,
operation: operation!,
parentIndexPath: indexPath.join('-'),
parentIsLocalized: parentIsLocalized || field.localized || false,
parentPath: path.join('.'),
parentSchemaPath: schemaPath.join('.'),
req,
@@ -637,11 +644,13 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
collection,
context,
data,
field,
global,
indexPath,
operation,
originalDoc,
overrideAccess,
parentIsLocalized,
path,
previousValue,
req,
@@ -762,7 +771,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
if (subFieldFn && subFieldDataFn) {
const subFields = subFieldFn({ node, req })
const nodeSiblingData = subFieldDataFn({ node, req }) ?? {}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const nodeSiblingDoc = subFieldDataFn({ node: originalNodeIDMap[id]!, req }) ?? {}
if (subFields?.length) {
@@ -778,6 +787,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
operation,
overrideAccess: overrideAccess!,
parentIndexPath: indexPath.join('-'),
parentIsLocalized: parentIsLocalized || field.localized || false,
parentPath: path.join('.'),
parentSchemaPath: schemaPath.join('.'),
req,

View File

@@ -8,6 +8,7 @@ import { recurseNodes } from '../utilities/forEachNodeRecursively.js'
export type Args = {
editorPopulationPromises: Map<string, Array<PopulationPromise>>
parentIsLocalized: boolean
} & Parameters<
NonNullable<RichTextAdapter<SerializedEditorState, AdapterProps>['graphQLPopulationPromises']>
>[0]
@@ -26,6 +27,7 @@ export const populateLexicalPopulationPromises = ({
findMany,
flattenLocales,
overrideAccess,
parentIsLocalized,
populationPromises,
req,
showHiddenFields,
@@ -54,6 +56,7 @@ export const populateLexicalPopulationPromises = ({
flattenLocales,
node,
overrideAccess: overrideAccess!,
parentIsLocalized,
populationPromises,
req,
showHiddenFields,

View File

@@ -22,6 +22,7 @@ type NestedRichTextFieldsArgs = {
findMany: boolean
flattenLocales: boolean
overrideAccess: boolean
parentIsLocalized: boolean
populationPromises: Promise<void>[]
req: PayloadRequest
showHiddenFields: boolean
@@ -39,6 +40,7 @@ export const recursivelyPopulateFieldsForGraphQL = ({
findMany,
flattenLocales,
overrideAccess = false,
parentIsLocalized,
populationPromises,
req,
showHiddenFields,
@@ -60,6 +62,7 @@ export const recursivelyPopulateFieldsForGraphQL = ({
locale: req.locale!,
overrideAccess,
parentIndexPath: '',
parentIsLocalized,
parentPath: '',
parentSchemaPath: '',
populationPromises, // This is not the same as populationPromises passed into this recurseNestedFields. These are just promises resolved at the very end.

View File

@@ -156,6 +156,7 @@ export const richTextRelationshipPromise = ({
draft,
field,
overrideAccess,
parentIsLocalized,
populateArg,
populationPromises,
req,

View File

@@ -119,6 +119,7 @@ export function slateEditor(
findMany,
flattenLocales,
overrideAccess,
parentIsLocalized,
populationPromises,
req,
showHiddenFields,
@@ -140,6 +141,7 @@ export function slateEditor(
findMany,
flattenLocales,
overrideAccess,
parentIsLocalized,
populationPromises,
req,
showHiddenFields,
@@ -159,6 +161,7 @@ export function slateEditor(
findMany,
flattenLocales,
overrideAccess,
parentIsLocalized,
populate,
populationPromises,
req,
@@ -183,6 +186,7 @@ export function slateEditor(
findMany,
flattenLocales,
overrideAccess,
parentIsLocalized,
populateArg: populate,
populationPromises,
req,

View File

@@ -7,7 +7,7 @@ import {
formatErrors,
type PayloadRequest,
} from 'payload'
import { fieldAffectsData, tabHasName } from 'payload/shared'
import { fieldAffectsData, fieldShouldBeLocalized, tabHasName } from 'payload/shared'
const ObjectId = (ObjectIdImport.default ||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
@@ -27,6 +27,7 @@ function iterateFields(
fromLocaleData: Data,
toLocaleData: Data,
req: PayloadRequest,
parentIsLocalized: boolean,
): void {
fields.map((field) => {
if (fieldAffectsData(field)) {
@@ -48,11 +49,17 @@ function iterateFields(
toLocaleData[field.name].map((item: Data, index: number) => {
if (fromLocaleData[field.name]?.[index]) {
// Generate new IDs if the field is localized to prevent errors with relational DBs.
if (field.localized) {
if (fieldShouldBeLocalized({ field, parentIsLocalized })) {
toLocaleData[field.name][index].id = new ObjectId().toHexString()
}
iterateFields(field.fields, fromLocaleData[field.name][index], item, req)
iterateFields(
field.fields,
fromLocaleData[field.name][index],
item,
req,
parentIsLocalized || field.localized,
)
}
})
}
@@ -80,12 +87,18 @@ function iterateFields(
) as FlattenedBlock | undefined)
// Generate new IDs if the field is localized to prevent errors with relational DBs.
if (field.localized) {
if (fieldShouldBeLocalized({ field, parentIsLocalized })) {
toLocaleData[field.name][index].id = new ObjectId().toHexString()
}
if (block?.fields?.length) {
iterateFields(block?.fields, fromLocaleData[field.name][index], blockData, req)
iterateFields(
block?.fields,
fromLocaleData[field.name][index],
blockData,
req,
parentIsLocalized || field.localized,
)
}
})
}
@@ -118,7 +131,13 @@ function iterateFields(
case 'group': {
if (field.name in toLocaleData && fromLocaleData?.[field.name] !== undefined) {
iterateFields(field.fields, fromLocaleData[field.name], toLocaleData[field.name], req)
iterateFields(
field.fields,
fromLocaleData[field.name],
toLocaleData[field.name],
req,
parentIsLocalized || field.localized,
)
}
break
}
@@ -127,17 +146,23 @@ function iterateFields(
switch (field.type) {
case 'collapsible':
case 'row':
iterateFields(field.fields, fromLocaleData, toLocaleData, req)
iterateFields(field.fields, fromLocaleData, toLocaleData, req, parentIsLocalized)
break
case 'tabs':
field.tabs.map((tab) => {
if (tabHasName(tab)) {
if (tab.name in toLocaleData && fromLocaleData?.[tab.name] !== undefined) {
iterateFields(tab.fields, fromLocaleData[tab.name], toLocaleData[tab.name], req)
iterateFields(
tab.fields,
fromLocaleData[tab.name],
toLocaleData[tab.name],
req,
parentIsLocalized,
)
}
} else {
iterateFields(tab.fields, fromLocaleData, toLocaleData, req)
iterateFields(tab.fields, fromLocaleData, toLocaleData, req, parentIsLocalized)
}
})
break
@@ -151,8 +176,9 @@ function mergeData(
toLocaleData: Data,
fields: Field[],
req: PayloadRequest,
parentIsLocalized: boolean,
): Data {
iterateFields(fields, fromLocaleData, toLocaleData, req)
iterateFields(fields, fromLocaleData, toLocaleData, req, parentIsLocalized)
return toLocaleData
}
@@ -272,6 +298,7 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => {
toLocaleData.value,
globals[globalSlug].config.fields,
req,
false,
),
locale: toLocale,
overrideAccess: false,
@@ -288,6 +315,7 @@ export const copyDataFromLocale = async (args: CopyDataFromLocaleArgs) => {
toLocaleData.value,
collections[collectionSlug].config.fields,
req,
false,
),
locale: toLocale,
overrideAccess: false,

View File

@@ -83,7 +83,7 @@ export interface Config {
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: number;
defaultIDType: string;
};
globals: {
menu: Menu;
@@ -123,7 +123,7 @@ export interface UserAuthOperations {
* via the `definition` "posts".
*/
export interface Post {
id: number;
id: string;
title?: string | null;
content?: {
root: {
@@ -148,7 +148,7 @@ export interface Post {
* via the `definition` "media".
*/
export interface Media {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -192,7 +192,7 @@ export interface Media {
* via the `definition` "users".
*/
export interface User {
id: number;
id: string;
updatedAt: string;
createdAt: string;
email: string;
@@ -209,24 +209,24 @@ export interface User {
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: number;
id: string;
document?:
| ({
relationTo: 'posts';
value: number | Post;
value: string | Post;
} | null)
| ({
relationTo: 'media';
value: number | Media;
value: string | Media;
} | null)
| ({
relationTo: 'users';
value: number | User;
value: string | User;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: number | User;
value: string | User;
};
updatedAt: string;
createdAt: string;
@@ -236,10 +236,10 @@ export interface PayloadLockedDocument {
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: number;
id: string;
user: {
relationTo: 'users';
value: number | User;
value: string | User;
};
key?: string | null;
value?:
@@ -259,7 +259,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: number;
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;
@@ -378,7 +378,7 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
* via the `definition` "menu".
*/
export interface Menu {
id: number;
id: string;
globalText?: string | null;
updatedAt?: string | null;
createdAt?: string | null;

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