Skip to content

Commit 3e65111

Browse files
authored
fix(plugin-import-export): csv export & preview showing full documents for hasMany monomorphic relationships instead of just ID (#13465)
### What? Fixes an issue where CSV exports and the preview table displayed all fields of documents in hasMany monomorphic relationships instead of only their IDs. ### Why? This caused cluttered output and inconsistent CSV formats, since only IDs should be exported for hasMany monomorphic relationships. ### How? Added explicit `toCSV` handling for all relationship types in `getCustomFieldFunctions`, updated `flattenObject` to delegate to these handlers, and adjusted `getFlattenedFieldKeys` to generate the correct headers.
1 parent 0e8a6c0 commit 3e65111

File tree

7 files changed

+106
-33
lines changed

7 files changed

+106
-33
lines changed

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

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,44 @@ export const flattenObject = ({
2222
const newKey = prefix ? `${prefix}_${key}` : key
2323

2424
if (Array.isArray(value)) {
25+
// If a custom toCSV function exists for this array field, run it first.
26+
// If it produces output, skip per-item handling; otherwise, fall back.
27+
if (toCSVFunctions?.[newKey]) {
28+
try {
29+
const result = toCSVFunctions[newKey]({
30+
columnName: newKey,
31+
data: row,
32+
doc,
33+
row,
34+
siblingDoc,
35+
value, // whole array
36+
})
37+
38+
if (typeof result !== 'undefined') {
39+
// Custom function returned a single value for this array field.
40+
row[newKey] = result
41+
return
42+
}
43+
44+
// If the custom function wrote any keys for this field, consider it handled.
45+
for (const k in row) {
46+
if (k === newKey || k.startsWith(`${newKey}_`)) {
47+
return
48+
}
49+
}
50+
// Otherwise, fall through to per-item handling.
51+
} catch (error) {
52+
throw new Error(
53+
`Error in toCSVFunction for array "${newKey}": ${JSON.stringify(value)}\n${
54+
(error as Error).message
55+
}`,
56+
)
57+
}
58+
}
59+
2560
value.forEach((item, index) => {
2661
if (typeof item === 'object' && item !== null) {
2762
const blockType = typeof item.blockType === 'string' ? item.blockType : undefined
28-
2963
const itemPrefix = blockType ? `${newKey}_${index}_${blockType}` : `${newKey}_${index}`
3064

3165
// Case: hasMany polymorphic relationships
@@ -40,35 +74,15 @@ export const flattenObject = ({
4074
return
4175
}
4276

77+
// Fallback: deep-flatten nested objects
4378
flatten(item, itemPrefix)
4479
} else {
45-
if (toCSVFunctions?.[newKey]) {
46-
const columnName = `${newKey}_${index}`
47-
try {
48-
const result = toCSVFunctions[newKey]({
49-
columnName,
50-
data: row,
51-
doc,
52-
row,
53-
siblingDoc,
54-
value: item,
55-
})
56-
if (typeof result !== 'undefined') {
57-
row[columnName] = result
58-
}
59-
} catch (error) {
60-
throw new Error(
61-
`Error in toCSVFunction for array item "${columnName}": ${JSON.stringify(item)}\n${
62-
(error as Error).message
63-
}`,
64-
)
65-
}
66-
} else {
67-
row[`${newKey}_${index}`] = item
68-
}
80+
// Primitive array item.
81+
row[`${newKey}_${index}`] = item
6982
}
7083
})
7184
} else if (typeof value === 'object' && value !== null) {
85+
// Object field: use custom toCSV if present, else recurse.
7286
if (!toCSVFunctions?.[newKey]) {
7387
flatten(value, newKey)
7488
} else {
@@ -86,7 +100,9 @@ export const flattenObject = ({
86100
}
87101
} catch (error) {
88102
throw new Error(
89-
`Error in toCSVFunction for nested object "${newKey}": ${JSON.stringify(value)}\n${(error as Error).message}`,
103+
`Error in toCSVFunction for nested object "${newKey}": ${JSON.stringify(value)}\n${
104+
(error as Error).message
105+
}`,
90106
)
91107
}
92108
}
@@ -106,7 +122,9 @@ export const flattenObject = ({
106122
}
107123
} catch (error) {
108124
throw new Error(
109-
`Error in toCSVFunction for field "${newKey}": ${JSON.stringify(value)}\n${(error as Error).message}`,
125+
`Error in toCSVFunction for field "${newKey}": ${JSON.stringify(value)}\n${
126+
(error as Error).message
127+
}`,
110128
)
111129
}
112130
} else {

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,21 @@ export const getCustomFieldFunctions = ({ fields }: Args): Record<string, ToCSVF
5353
// monomorphic many
5454
// @ts-expect-error ref is untyped
5555
result[`${ref.prefix}${field.name}`] = ({
56+
data,
5657
value,
5758
}: {
58-
value: Record<string, unknown>[]
59-
}) =>
60-
value.map((val: number | Record<string, unknown> | string) =>
61-
typeof val === 'object' ? val.id : val,
62-
)
59+
data: Record<string, unknown>
60+
value: Array<number | Record<string, any> | string> | undefined
61+
}) => {
62+
if (Array.isArray(value)) {
63+
value.forEach((val, i) => {
64+
const id = typeof val === 'object' && val ? val.id : val
65+
// @ts-expect-error ref is untyped
66+
data[`${ref.prefix}${field.name}_${i}_id`] = id
67+
})
68+
}
69+
return undefined // prevents further flattening
70+
}
6371
} else {
6472
// polymorphic many
6573
// @ts-expect-error ref is untyped

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const getFlattenedFieldKeys = (fields: FieldWithPresentational[], prefix
5252
keys.push(`${fullKey}_0_relationTo`, `${fullKey}_0_id`)
5353
} else {
5454
// hasMany monomorphic
55-
keys.push(`${fullKey}_0`)
55+
keys.push(`${fullKey}_0_id`)
5656
}
5757
} else {
5858
if (Array.isArray(field.relationTo)) {

test/plugin-import-export/collections/Pages.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,12 @@ export const Pages: CollectionConfig = {
221221
relationTo: ['users', 'posts'],
222222
hasMany: true,
223223
},
224+
{
225+
name: 'hasManyMonomorphic',
226+
type: 'relationship',
227+
relationTo: 'posts',
228+
hasMany: true,
229+
},
224230
{
225231
type: 'collapsible',
226232
label: 'Collapsible Field',

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,35 @@ describe('@payloadcms/plugin-import-export', () => {
598598
expect(data[0].hasManyPolymorphic_1_relationTo).toBe('posts')
599599
})
600600

601+
it('should export hasMany monomorphic relationship fields to CSV', async () => {
602+
const doc = await payload.create({
603+
collection: 'exports',
604+
user,
605+
data: {
606+
collectionSlug: 'pages',
607+
fields: ['id', 'hasManyMonomorphic'],
608+
format: 'csv',
609+
where: {
610+
title: { contains: 'Monomorphic' },
611+
},
612+
},
613+
})
614+
615+
const exportDoc = await payload.findByID({
616+
collection: 'exports',
617+
id: doc.id,
618+
})
619+
620+
expect(exportDoc.filename).toBeDefined()
621+
const expectedPath = path.join(dirname, './uploads', exportDoc.filename as string)
622+
const data = await readCSV(expectedPath)
623+
624+
// hasManyMonomorphic
625+
expect(data[0].hasManyMonomorphic_0_id).toBeDefined()
626+
expect(data[0].hasManyMonomorphic_0_relationTo).toBeUndefined()
627+
expect(data[0].hasManyMonomorphic_0_title).toBeUndefined()
628+
})
629+
601630
// disabled so we don't always run a massive test
602631
it.skip('should create a file from a large set of collection documents', async () => {
603632
const allPromises = []

test/plugin-import-export/payload-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ export interface Page {
242242
}
243243
)[]
244244
| null;
245+
hasManyMonomorphic?: (string | Post)[] | null;
245246
textFieldInCollapsible?: string | null;
246247
updatedAt: string;
247248
createdAt: string;
@@ -580,6 +581,7 @@ export interface PagesSelect<T extends boolean = true> {
580581
excerpt?: T;
581582
hasOnePolymorphic?: T;
582583
hasManyPolymorphic?: T;
584+
hasManyMonomorphic?: T;
583585
textFieldInCollapsible?: T;
584586
updatedAt?: T;
585587
createdAt?: T;

test/plugin-import-export/seed/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,16 @@ export const seed = async (payload: Payload): Promise<boolean> => {
159159
})
160160
}
161161

162+
for (let i = 0; i < 2; i++) {
163+
await payload.create({
164+
collection: 'pages',
165+
data: {
166+
title: `Monomorphic ${i}`,
167+
hasManyMonomorphic: [posts[1]?.id ?? ''],
168+
},
169+
})
170+
}
171+
162172
for (let i = 0; i < 5; i++) {
163173
await payload.create({
164174
collection: 'pages',

0 commit comments

Comments
 (0)