Skip to content

Commit c6105f1

Browse files
authored
fix(plugin-import-export): flattening logic for polymorphic relationships in CSV exports (#13094)
### What? Improves the flattening logic used in the import-export plugin to correctly handle polymorphic relationships (both `hasOne` and `hasMany`) when generating CSV columns. ### Why? Previously, `hasMany` polymorphic relationships would flatten their full `value` object recursively, resulting in unwanted keys like `createdAt`, `title`, `email`, etc. This change ensures that only the `id` and `relationTo` fields are included, matching how `hasOne` polymorphic fields already behave. ### How? - Updated `flattenObject` to special-case `hasMany` polymorphic relationships and extract only `relationTo` and `id` per index. - Refined `getFlattenedFieldKeys` to return correct column keys for polymorphic fields: - `hasMany polymorphic → name_0_relationTo`, `name_0_id` - `hasOne polymorphic → name_relationTo`, `name_id` - `monomorphic → name` or `name_0` - **Added try/catch blocks** around `toCSVFunctions` calls in `flattenObject`, with descriptive error messages including the column path and input value. This improves debuggability if a custom `toCSV` function throws.
1 parent 0806ee1 commit c6105f1

File tree

6 files changed

+86
-55
lines changed

6 files changed

+86
-55
lines changed

packages/plugin-import-export/src/export/createExport.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ export const createExport = async (args: CreateExportArgs) => {
106106

107107
const toCSVFunctions = getCustomFieldFunctions({
108108
fields: collectionConfig.flattenedFields,
109-
select,
110109
})
111110

112111
if (download) {

packages/plugin-import-export/src/export/flattenObject.ts

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,38 @@ export const flattenObject = ({
2424
if (Array.isArray(value)) {
2525
value.forEach((item, index) => {
2626
if (typeof item === 'object' && item !== null) {
27+
// Case: hasMany polymorphic relationships
28+
if (
29+
'relationTo' in item &&
30+
'value' in item &&
31+
typeof item.value === 'object' &&
32+
item.value !== null
33+
) {
34+
row[`${`${newKey}_${index}`}_relationTo`] = item.relationTo
35+
row[`${`${newKey}_${index}`}_id`] = item.value.id
36+
return
37+
}
38+
2739
flatten(item, `${newKey}_${index}`)
2840
} else {
2941
if (toCSVFunctions?.[newKey]) {
3042
const columnName = `${newKey}_${index}`
31-
const result = toCSVFunctions[newKey]({
32-
columnName,
33-
data: row,
34-
doc,
35-
row,
36-
siblingDoc,
37-
value: item,
38-
})
39-
if (typeof result !== 'undefined') {
40-
row[columnName] = result
43+
try {
44+
const result = toCSVFunctions[newKey]({
45+
columnName,
46+
data: row,
47+
doc,
48+
row,
49+
siblingDoc,
50+
value: item,
51+
})
52+
if (typeof result !== 'undefined') {
53+
row[columnName] = result
54+
}
55+
} catch (error) {
56+
throw new Error(
57+
`Error in toCSVFunction for array item "${columnName}": ${JSON.stringify(item)}\n${(error as Error).message}`,
58+
)
4159
}
4260
} else {
4361
row[`${newKey}_${index}`] = item
@@ -48,30 +66,42 @@ export const flattenObject = ({
4866
if (!toCSVFunctions?.[newKey]) {
4967
flatten(value, newKey)
5068
} else {
51-
const result = toCSVFunctions[newKey]({
52-
columnName: newKey,
53-
data: row,
54-
doc,
55-
row,
56-
siblingDoc,
57-
value,
58-
})
59-
if (typeof result !== 'undefined') {
60-
row[newKey] = result
69+
try {
70+
const result = toCSVFunctions[newKey]({
71+
columnName: newKey,
72+
data: row,
73+
doc,
74+
row,
75+
siblingDoc,
76+
value,
77+
})
78+
if (typeof result !== 'undefined') {
79+
row[newKey] = result
80+
}
81+
} catch (error) {
82+
throw new Error(
83+
`Error in toCSVFunction for nested object "${newKey}": ${JSON.stringify(value)}\n${(error as Error).message}`,
84+
)
6185
}
6286
}
6387
} else {
6488
if (toCSVFunctions?.[newKey]) {
65-
const result = toCSVFunctions[newKey]({
66-
columnName: newKey,
67-
data: row,
68-
doc,
69-
row,
70-
siblingDoc,
71-
value,
72-
})
73-
if (typeof result !== 'undefined') {
74-
row[newKey] = result
89+
try {
90+
const result = toCSVFunctions[newKey]({
91+
columnName: newKey,
92+
data: row,
93+
doc,
94+
row,
95+
siblingDoc,
96+
value,
97+
})
98+
if (typeof result !== 'undefined') {
99+
row[newKey] = result
100+
}
101+
} catch (error) {
102+
throw new Error(
103+
`Error in toCSVFunction for field "${newKey}": ${JSON.stringify(value)}\n${(error as Error).message}`,
104+
)
75105
}
76106
} else {
77107
row[newKey] = value

packages/plugin-import-export/src/export/getCustomFieldFunctions.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,12 @@
1-
import {
2-
type FlattenedField,
3-
type SelectIncludeType,
4-
traverseFields,
5-
type TraverseFieldsCallback,
6-
} from 'payload'
1+
import { type FlattenedField, traverseFields, type TraverseFieldsCallback } from 'payload'
72

83
import type { ToCSVFunction } from '../types.js'
94

105
type Args = {
116
fields: FlattenedField[]
12-
select: SelectIncludeType | undefined
137
}
148

15-
export const getCustomFieldFunctions = ({
16-
fields,
17-
select,
18-
}: Args): Record<string, ToCSVFunction> => {
9+
export const getCustomFieldFunctions = ({ fields }: Args): Record<string, ToCSVFunction> => {
1910
const result: Record<string, ToCSVFunction> = {}
2011

2112
const buildCustomFunctions: TraverseFieldsCallback = ({ field, parentRef, ref }) => {
@@ -54,7 +45,7 @@ export const getCustomFieldFunctions = ({
5445
data[`${ref.prefix}${field.name}_relationTo`] = relationTo
5546
}
5647
}
57-
return undefined
48+
return undefined // prevents further flattening
5849
}
5950
}
6051
} else {
@@ -98,10 +89,6 @@ export const getCustomFieldFunctions = ({
9889
}
9990
}
10091
}
101-
102-
// TODO: do this so we only return the functions needed based on the select used
103-
////@ts-expect-error ref is untyped
104-
// ref.select = typeof select !== 'undefined' || select[field.name] ? select : {}
10592
}
10693

10794
traverseFields({ callback: buildCustomFunctions, fields })

packages/plugin-import-export/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ export const importExportPlugin =
110110

111111
const toCSVFunctions = getCustomFieldFunctions({
112112
fields: collection.config.fields as FlattenedField[],
113-
select,
114113
})
115114

116115
const possibleKeys = getFlattenedFieldKeys(collection.config.fields as FlattenedField[])

packages/plugin-import-export/src/utilities/getFlattenedFieldKeys.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@ export const getFlattenedFieldKeys = (fields: FieldWithPresentational[], prefix
1616
const keys: string[] = []
1717

1818
fields.forEach((field) => {
19-
if (!('name' in field) || typeof field.name !== 'string') {
19+
const fieldHasToCSVFunction =
20+
'custom' in field &&
21+
typeof field.custom === 'object' &&
22+
'plugin-import-export' in field.custom &&
23+
field.custom['plugin-import-export']?.toCSV
24+
25+
if (!('name' in field) || typeof field.name !== 'string' || fieldHasToCSVFunction) {
2026
return
2127
}
2228

@@ -41,11 +47,21 @@ export const getFlattenedFieldKeys = (fields: FieldWithPresentational[], prefix
4147
break
4248
case 'relationship':
4349
if (field.hasMany) {
44-
// e.g. hasManyPolymorphic_0_value_id
45-
keys.push(`${name}_0_relationTo`, `${name}_0_value_id`)
50+
if (Array.isArray(field.relationTo)) {
51+
// hasMany polymorphic
52+
keys.push(`${name}_0_relationTo`, `${name}_0_id`)
53+
} else {
54+
// hasMany monomorphic
55+
keys.push(`${name}_0`)
56+
}
4657
} else {
47-
// e.g. hasOnePolymorphic_id
48-
keys.push(`${name}_id`, `${name}_relationTo`)
58+
if (Array.isArray(field.relationTo)) {
59+
// hasOne polymorphic
60+
keys.push(`${name}_relationTo`, `${name}_id`)
61+
} else {
62+
// hasOne monomorphic
63+
keys.push(name)
64+
}
4965
}
5066
break
5167
case 'tabs':

test/plugin-import-export/int.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -569,9 +569,9 @@ describe('@payloadcms/plugin-import-export', () => {
569569
expect(data[0].hasOnePolymorphic_relationTo).toBe('posts')
570570

571571
// hasManyPolymorphic
572-
expect(data[0].hasManyPolymorphic_0_value_id).toBeDefined()
572+
expect(data[0].hasManyPolymorphic_0_id).toBeDefined()
573573
expect(data[0].hasManyPolymorphic_0_relationTo).toBe('users')
574-
expect(data[0].hasManyPolymorphic_1_value_id).toBeDefined()
574+
expect(data[0].hasManyPolymorphic_1_id).toBeDefined()
575575
expect(data[0].hasManyPolymorphic_1_relationTo).toBe('posts')
576576
})
577577

0 commit comments

Comments
 (0)