feat(plugin-import-export): add custom toCSV function on fields (#12533)
This makes it possible to add custom logic into how we map the document
data into the CSV data on a field-by-field basis.
- Allow custom data transformation to be added to
`custom.['plugin-import-export'].toCSV inside the field config
- Add type declaration to FieldCustom to improve types
- Export with `depth: 1`
Example:
```ts
{
name: 'customRelationship',
type: 'relationship',
relationTo: 'users',
custom: {
'plugin-import-export': {
toCSV: ({ value, columnName, row, siblingDoc, doc }) => {
row[`${columnName}_id`] = value.id
row[`${columnName}_email`] = value.email
},
},
},
},
```
This commit is contained in:
@@ -551,4 +551,4 @@ Below are some common troubleshooting tips. To help other developers, please con
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -128,6 +128,7 @@ import type {
|
|||||||
CollectionSlug,
|
CollectionSlug,
|
||||||
DateFieldValidation,
|
DateFieldValidation,
|
||||||
EmailFieldValidation,
|
EmailFieldValidation,
|
||||||
|
FieldCustom,
|
||||||
JSONFieldValidation,
|
JSONFieldValidation,
|
||||||
PointFieldValidation,
|
PointFieldValidation,
|
||||||
RadioFieldValidation,
|
RadioFieldValidation,
|
||||||
@@ -482,7 +483,7 @@ export interface FieldBase {
|
|||||||
}
|
}
|
||||||
admin?: Admin
|
admin?: Admin
|
||||||
/** Extension point to add your custom data. Server only. */
|
/** Extension point to add your custom data. Server only. */
|
||||||
custom?: Record<string, any>
|
custom?: FieldCustom
|
||||||
defaultValue?: DefaultValue
|
defaultValue?: DefaultValue
|
||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
hooks?: {
|
hooks?: {
|
||||||
|
|||||||
@@ -1238,6 +1238,9 @@ export {
|
|||||||
} from './fields/config/client.js'
|
} from './fields/config/client.js'
|
||||||
|
|
||||||
export { sanitizeFields } from './fields/config/sanitize.js'
|
export { sanitizeFields } from './fields/config/sanitize.js'
|
||||||
|
|
||||||
|
export interface FieldCustom extends Record<string, any> {}
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
AdminClient,
|
AdminClient,
|
||||||
ArrayField,
|
ArrayField,
|
||||||
|
|||||||
@@ -304,12 +304,12 @@ export const traverseFields = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.type !== 'tab' && (fieldHasSubFields(field) || field.type === 'blocks')) {
|
if (field.type === 'tab' || fieldHasSubFields(field) || field.type === 'blocks') {
|
||||||
if ('name' in field && field.name) {
|
if ('name' in field && field.name) {
|
||||||
currentParentRef = currentRef
|
currentParentRef = currentRef
|
||||||
if (!ref[field.name]) {
|
if (!ref[field.name]) {
|
||||||
if (fillEmpty) {
|
if (fillEmpty) {
|
||||||
if (field.type === 'group') {
|
if (field.type === 'group' || field.type === 'tab') {
|
||||||
if (fieldShouldBeLocalized({ field, parentIsLocalized: parentIsLocalized! })) {
|
if (fieldShouldBeLocalized({ field, parentIsLocalized: parentIsLocalized! })) {
|
||||||
ref[field.name] = {
|
ref[field.name] = {
|
||||||
en: {},
|
en: {},
|
||||||
@@ -334,7 +334,7 @@ export const traverseFields = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
field.type === 'group' &&
|
(field.type === 'tab' || field.type === 'group') &&
|
||||||
fieldShouldBeLocalized({ field, parentIsLocalized: parentIsLocalized! }) &&
|
fieldShouldBeLocalized({ field, parentIsLocalized: parentIsLocalized! }) &&
|
||||||
currentRef &&
|
currentRef &&
|
||||||
typeof currentRef === 'object'
|
typeof currentRef === 'object'
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { APIError } from 'payload'
|
|||||||
import { Readable } from 'stream'
|
import { Readable } from 'stream'
|
||||||
|
|
||||||
import { flattenObject } from './flattenObject.js'
|
import { flattenObject } from './flattenObject.js'
|
||||||
|
import { getCustomFieldFunctions } from './getCustomFieldFunctions.js'
|
||||||
import { getFilename } from './getFilename.js'
|
import { getFilename } from './getFilename.js'
|
||||||
import { getSelect } from './getSelect.js'
|
import { getSelect } from './getSelect.js'
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ export const createExport = async (args: CreateExportArgs) => {
|
|||||||
|
|
||||||
const name = `${nameArg ?? `${getFilename()}-${collectionSlug}`}.${format}`
|
const name = `${nameArg ?? `${getFilename()}-${collectionSlug}`}.${format}`
|
||||||
const isCSV = format === 'csv'
|
const isCSV = format === 'csv'
|
||||||
|
const select = Array.isArray(fields) && fields.length > 0 ? getSelect(fields) : undefined
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
req.payload.logger.info({ message: 'Export configuration:', name, isCSV, locale })
|
req.payload.logger.info({ message: 'Export configuration:', name, isCSV, locale })
|
||||||
@@ -86,13 +88,13 @@ export const createExport = async (args: CreateExportArgs) => {
|
|||||||
|
|
||||||
const findArgs = {
|
const findArgs = {
|
||||||
collection: collectionSlug,
|
collection: collectionSlug,
|
||||||
depth: 0,
|
depth: 1,
|
||||||
draft: drafts === 'yes',
|
draft: drafts === 'yes',
|
||||||
limit: 100,
|
limit: 100,
|
||||||
locale,
|
locale,
|
||||||
overrideAccess: false,
|
overrideAccess: false,
|
||||||
page: 0,
|
page: 0,
|
||||||
select: Array.isArray(fields) && fields.length > 0 ? getSelect(fields) : undefined,
|
select,
|
||||||
sort,
|
sort,
|
||||||
user,
|
user,
|
||||||
where,
|
where,
|
||||||
@@ -104,6 +106,11 @@ export const createExport = async (args: CreateExportArgs) => {
|
|||||||
|
|
||||||
let result: PaginatedDocs = { hasNextPage: true } as PaginatedDocs
|
let result: PaginatedDocs = { hasNextPage: true } as PaginatedDocs
|
||||||
|
|
||||||
|
const toCSVFunctions = getCustomFieldFunctions({
|
||||||
|
fields: collectionConfig.flattenedFields,
|
||||||
|
select,
|
||||||
|
})
|
||||||
|
|
||||||
if (download) {
|
if (download) {
|
||||||
if (debug) {
|
if (debug) {
|
||||||
req.payload.logger.info('Starting download stream')
|
req.payload.logger.info('Starting download stream')
|
||||||
@@ -120,7 +127,7 @@ export const createExport = async (args: CreateExportArgs) => {
|
|||||||
`Processing batch ${findArgs.page + 1} with ${result.docs.length} documents`,
|
`Processing batch ${findArgs.page + 1} with ${result.docs.length} documents`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const csvInput = result.docs.map((doc) => flattenObject({ doc, fields }))
|
const csvInput = result.docs.map((doc) => flattenObject({ doc, fields, toCSVFunctions }))
|
||||||
const csvString = stringify(csvInput, { header: isFirstBatch })
|
const csvString = stringify(csvInput, { header: isFirstBatch })
|
||||||
this.push(encoder.encode(csvString))
|
this.push(encoder.encode(csvString))
|
||||||
isFirstBatch = false
|
isFirstBatch = false
|
||||||
@@ -164,7 +171,7 @@ export const createExport = async (args: CreateExportArgs) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isCSV) {
|
if (isCSV) {
|
||||||
const csvInput = result.docs.map((doc) => flattenObject({ doc, fields }))
|
const csvInput = result.docs.map((doc) => flattenObject({ doc, fields, toCSVFunctions }))
|
||||||
outputData.push(stringify(csvInput, { header: isFirstBatch }))
|
outputData.push(stringify(csvInput, { header: isFirstBatch }))
|
||||||
isFirstBatch = false
|
isFirstBatch = false
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
import type { Document } from 'payload'
|
import type { Document } from 'payload'
|
||||||
|
|
||||||
|
import type { ToCSVFunction } from '../types.js'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
doc: Document
|
doc: Document
|
||||||
fields?: string[]
|
fields?: string[]
|
||||||
prefix?: string
|
prefix?: string
|
||||||
|
toCSVFunctions: Record<string, ToCSVFunction>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const flattenObject = ({ doc, fields, prefix }: Args): Record<string, unknown> => {
|
export const flattenObject = ({
|
||||||
const result: Record<string, unknown> = {}
|
doc,
|
||||||
|
fields,
|
||||||
|
prefix,
|
||||||
|
toCSVFunctions,
|
||||||
|
}: Args): Record<string, unknown> => {
|
||||||
|
const row: Record<string, unknown> = {}
|
||||||
|
|
||||||
const flatten = (doc: Document, prefix?: string) => {
|
const flatten = (siblingDoc: Document, prefix?: string) => {
|
||||||
Object.entries(doc).forEach(([key, value]) => {
|
Object.entries(siblingDoc).forEach(([key, value]) => {
|
||||||
const newKey = prefix ? `${prefix}_${key}` : key
|
const newKey = prefix ? `${prefix}_${key}` : key
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
@@ -18,13 +26,44 @@ export const flattenObject = ({ doc, fields, prefix }: Args): Record<string, unk
|
|||||||
if (typeof item === 'object' && item !== null) {
|
if (typeof item === 'object' && item !== null) {
|
||||||
flatten(item, `${newKey}_${index}`)
|
flatten(item, `${newKey}_${index}`)
|
||||||
} else {
|
} else {
|
||||||
result[`${newKey}_${index}`] = item
|
if (toCSVFunctions?.[newKey]) {
|
||||||
|
const columnName = `${newKey}_${index}`
|
||||||
|
row[columnName] = toCSVFunctions[newKey]({
|
||||||
|
columnName,
|
||||||
|
doc,
|
||||||
|
row,
|
||||||
|
siblingDoc,
|
||||||
|
value: item,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
row[`${newKey}_${index}`] = item
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if (typeof value === 'object' && value !== null) {
|
} else if (typeof value === 'object' && value !== null) {
|
||||||
flatten(value, newKey)
|
if (!toCSVFunctions?.[newKey]) {
|
||||||
|
flatten(value, newKey)
|
||||||
|
} else {
|
||||||
|
row[newKey] = toCSVFunctions[newKey]({
|
||||||
|
columnName: newKey,
|
||||||
|
doc,
|
||||||
|
row,
|
||||||
|
siblingDoc,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
result[newKey] = value
|
if (toCSVFunctions?.[newKey]) {
|
||||||
|
row[newKey] = toCSVFunctions[newKey]({
|
||||||
|
columnName: newKey,
|
||||||
|
doc,
|
||||||
|
row,
|
||||||
|
siblingDoc,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
row[newKey] = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -41,14 +80,14 @@ export const flattenObject = ({ doc, fields, prefix }: Args): Record<string, unk
|
|||||||
}
|
}
|
||||||
|
|
||||||
fields.forEach((field) => {
|
fields.forEach((field) => {
|
||||||
if (result[field.replace(/\./g, '_')]) {
|
if (row[field.replace(/\./g, '_')]) {
|
||||||
const sanitizedField = field.replace(/\./g, '_')
|
const sanitizedField = field.replace(/\./g, '_')
|
||||||
orderedResult[sanitizedField] = result[sanitizedField]
|
orderedResult[sanitizedField] = row[sanitizedField]
|
||||||
} else {
|
} else {
|
||||||
const regex = fieldToRegex(field)
|
const regex = fieldToRegex(field)
|
||||||
Object.keys(result).forEach((key) => {
|
Object.keys(row).forEach((key) => {
|
||||||
if (regex.test(key)) {
|
if (regex.test(key)) {
|
||||||
orderedResult[key] = result[key]
|
orderedResult[key] = row[key]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -57,5 +96,5 @@ export const flattenObject = ({ doc, fields, prefix }: Args): Record<string, unk
|
|||||||
return orderedResult
|
return orderedResult
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return row
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import {
|
||||||
|
type FlattenedField,
|
||||||
|
type SelectIncludeType,
|
||||||
|
traverseFields,
|
||||||
|
type TraverseFieldsCallback,
|
||||||
|
} from 'payload'
|
||||||
|
|
||||||
|
import type { ToCSVFunction } from '../types.js'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
fields: FlattenedField[]
|
||||||
|
select: SelectIncludeType | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCustomFieldFunctions = ({
|
||||||
|
fields,
|
||||||
|
select,
|
||||||
|
}: Args): Record<string, ToCSVFunction> => {
|
||||||
|
const result: Record<string, ToCSVFunction> = {}
|
||||||
|
|
||||||
|
const buildCustomFunctions: TraverseFieldsCallback = ({ field, parentRef, ref }) => {
|
||||||
|
// @ts-expect-error ref is untyped
|
||||||
|
ref.prefix = parentRef.prefix || ''
|
||||||
|
if (field.type === 'group' || field.type === 'tab') {
|
||||||
|
// @ts-expect-error ref is untyped
|
||||||
|
const parentPrefix = parentRef?.prefix ? `${parentRef.prefix}_` : ''
|
||||||
|
// @ts-expect-error ref is untyped
|
||||||
|
ref.prefix = `${parentPrefix}${field.name}_`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof field.custom?.['plugin-import-export']?.toCSV === 'function') {
|
||||||
|
// @ts-expect-error ref is untyped
|
||||||
|
result[`${ref.prefix}${field.name}`] = field.custom['plugin-import-export']?.toCSV
|
||||||
|
} else if (field.type === 'relationship' || field.type === 'upload') {
|
||||||
|
if (field.hasMany !== true) {
|
||||||
|
if (!Array.isArray(field.relationTo)) {
|
||||||
|
// monomorphic single
|
||||||
|
// @ts-expect-error ref is untyped
|
||||||
|
result[`${ref.prefix}${field.name}`] = ({ value }) =>
|
||||||
|
typeof value === 'object' && value && 'id' in value ? value.id : value
|
||||||
|
} else {
|
||||||
|
// polymorphic single
|
||||||
|
// @ts-expect-error ref is untyped
|
||||||
|
result[`${ref.prefix}${field.name}`] = ({ data, value }) => {
|
||||||
|
// @ts-expect-error ref is untyped
|
||||||
|
data[`${ref.prefix}${field.name}_id`] = value.id
|
||||||
|
// @ts-expect-error ref is untyped
|
||||||
|
data[`${ref.prefix}${field.name}_relationTo`] = value.relationTo
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!Array.isArray(field.relationTo)) {
|
||||||
|
// monomorphic many
|
||||||
|
// @ts-expect-error ref is untyped
|
||||||
|
result[`${ref.prefix}${field.name}`] = ({
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
value: Record<string, unknown>[]
|
||||||
|
}) =>
|
||||||
|
value.map((val: number | Record<string, unknown> | string) =>
|
||||||
|
typeof val === 'object' ? val.id : val,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// polymorphic many
|
||||||
|
// @ts-expect-error ref is untyped
|
||||||
|
result[`${ref.prefix}${field.name}`] = ({
|
||||||
|
data,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
data: Record<string, unknown>
|
||||||
|
value: Record<string, unknown>[]
|
||||||
|
}) =>
|
||||||
|
value.map((val: number | Record<string, unknown> | string, i) => {
|
||||||
|
// @ts-expect-error ref is untyped
|
||||||
|
data[`${ref.prefix}${field.name}_${i}_id`] = val.id
|
||||||
|
// @ts-expect-error ref is untyped
|
||||||
|
data[`${ref.prefix}${field.name}_${i}_relationTo`] = val.relationTo
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: do this so we only return the functions needed based on the select used
|
||||||
|
////@ts-expect-error ref is untyped
|
||||||
|
// ref.select = typeof select !== 'undefined' || select[field.name] ? select : {}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverseFields({ callback: buildCustomFunctions, fields })
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -1,17 +1,13 @@
|
|||||||
import type { SelectType } from 'payload'
|
import type { SelectIncludeType } from 'payload'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes an input of array of string paths in dot notation and returns a select object
|
* Takes an input of array of string paths in dot notation and returns a select object
|
||||||
* example args: ['id', 'title', 'group.value', 'createdAt', 'updatedAt']
|
* example args: ['id', 'title', 'group.value', 'createdAt', 'updatedAt']
|
||||||
*/
|
*/
|
||||||
export const getSelect = (fields: string[]): SelectType => {
|
export const getSelect = (fields: string[]): SelectIncludeType => {
|
||||||
const select: SelectType = {}
|
const select: SelectIncludeType = {}
|
||||||
|
|
||||||
fields.forEach((field) => {
|
fields.forEach((field) => {
|
||||||
// TODO: this can likely be removed, the form was not saving, leaving in for now
|
|
||||||
if (!field) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const segments = field.split('.')
|
const segments = field.split('.')
|
||||||
let selectRef = select
|
let selectRef = select
|
||||||
|
|
||||||
@@ -22,7 +18,7 @@ export const getSelect = (fields: string[]): SelectType => {
|
|||||||
if (!selectRef[segment]) {
|
if (!selectRef[segment]) {
|
||||||
selectRef[segment] = {}
|
selectRef[segment] = {}
|
||||||
}
|
}
|
||||||
selectRef = selectRef[segment] as SelectType
|
selectRef = selectRef[segment] as SelectIncludeType
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export type { ImportExportPluginConfig } from '../types.js'
|
export type { ImportExportPluginConfig, ToCSVFunction } from '../types.js'
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
CollectionAfterChangeHook,
|
CollectionAfterChangeHook,
|
||||||
CollectionBeforeChangeHook,
|
|
||||||
CollectionBeforeOperationHook,
|
CollectionBeforeOperationHook,
|
||||||
CollectionConfig,
|
CollectionConfig,
|
||||||
Config,
|
Config,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Config, JobsConfig } from 'payload'
|
|||||||
import { deepMergeSimple } from 'payload'
|
import { deepMergeSimple } from 'payload'
|
||||||
|
|
||||||
import type { PluginDefaultTranslationsObject } from './translations/types.js'
|
import type { PluginDefaultTranslationsObject } from './translations/types.js'
|
||||||
import type { ImportExportPluginConfig } from './types.js'
|
import type { ImportExportPluginConfig, ToCSVFunction } from './types.js'
|
||||||
|
|
||||||
import { getCreateCollectionExportTask } from './export/getCreateExportCollectionTask.js'
|
import { getCreateCollectionExportTask } from './export/getCreateExportCollectionTask.js'
|
||||||
import { getExportCollection } from './getExportCollection.js'
|
import { getExportCollection } from './getExportCollection.js'
|
||||||
@@ -91,3 +91,11 @@ export const importExportPlugin =
|
|||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module 'payload' {
|
||||||
|
export interface FieldCustom {
|
||||||
|
'plugin-import-export'?: {
|
||||||
|
toCSVFunction?: ToCSVFunction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,3 +26,29 @@ export type ImportExportPluginConfig = {
|
|||||||
*/
|
*/
|
||||||
overrideExportCollection?: (collection: CollectionOverride) => CollectionOverride
|
overrideExportCollection?: (collection: CollectionOverride) => CollectionOverride
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom function used to modify the outgoing csv data by manipulating the data, siblingData or by returning the desired value
|
||||||
|
*/
|
||||||
|
export type ToCSVFunction = (args: {
|
||||||
|
/**
|
||||||
|
* The path of the column for the field, for arrays this includes the index (zero-based)
|
||||||
|
*/
|
||||||
|
columnName: string
|
||||||
|
/**
|
||||||
|
* The top level document
|
||||||
|
*/
|
||||||
|
doc: Document
|
||||||
|
/**
|
||||||
|
* The object data that can be manipulated to assign data to the CSV
|
||||||
|
*/
|
||||||
|
row: Record<string, unknown>
|
||||||
|
/**
|
||||||
|
* The document data at the level where it belongs
|
||||||
|
*/
|
||||||
|
siblingDoc: Record<string, unknown>
|
||||||
|
/**
|
||||||
|
* The data for the field.
|
||||||
|
*/
|
||||||
|
value: unknown
|
||||||
|
}) => unknown
|
||||||
|
|||||||
@@ -26,6 +26,31 @@ export const Pages: CollectionConfig = {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
localized: true,
|
localized: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'custom',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: 'my custom csv transformer',
|
||||||
|
custom: {
|
||||||
|
'plugin-import-export': {
|
||||||
|
toCSV: ({ value, columnName, row, siblingDoc }) => {
|
||||||
|
return value + ' toCSV'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customRelationship',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'users',
|
||||||
|
custom: {
|
||||||
|
'plugin-import-export': {
|
||||||
|
toCSV: ({ value, columnName, row, siblingDoc, doc }) => {
|
||||||
|
row[`${columnName}_id`] = value.id
|
||||||
|
row[`${columnName}_email`] = value.email
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'group',
|
name: 'group',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
@@ -53,6 +78,58 @@ export const Pages: CollectionConfig = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'custom',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: 'my custom csv transformer',
|
||||||
|
custom: {
|
||||||
|
'plugin-import-export': {
|
||||||
|
toCSV: ({ value, columnName, row, siblingDoc, doc }) => {
|
||||||
|
return value + ' toCSV'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tabs',
|
||||||
|
type: 'tabs',
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
label: 'No Name',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'tabToCSV',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: 'my custom csv transformer',
|
||||||
|
custom: {
|
||||||
|
'plugin-import-export': {
|
||||||
|
toCSV: ({ value, columnName, row, siblingDoc, doc }) => {
|
||||||
|
return value + ' toCSV'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'namedTab',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'tabToCSV',
|
||||||
|
type: 'text',
|
||||||
|
defaultValue: 'my custom csv transformer',
|
||||||
|
custom: {
|
||||||
|
'plugin-import-export': {
|
||||||
|
toCSV: ({ value, columnName, row, siblingDoc, doc }) => {
|
||||||
|
return value + ' toCSV'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -368,6 +368,47 @@ describe('@payloadcms/plugin-import-export', () => {
|
|||||||
expect(data[0].blocks_1_blockType).toStrictEqual('content')
|
expect(data[0].blocks_1_blockType).toStrictEqual('content')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should run custom toCSV function on a field', async () => {
|
||||||
|
const fields = [
|
||||||
|
'id',
|
||||||
|
'custom',
|
||||||
|
'group.custom',
|
||||||
|
'customRelationship',
|
||||||
|
'tabToCSV',
|
||||||
|
'namedTab.tabToCSV',
|
||||||
|
]
|
||||||
|
const doc = await payload.create({
|
||||||
|
collection: 'exports',
|
||||||
|
user,
|
||||||
|
data: {
|
||||||
|
collectionSlug: 'pages',
|
||||||
|
fields,
|
||||||
|
format: 'csv',
|
||||||
|
where: {
|
||||||
|
title: { contains: 'Custom ' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const exportDoc = await payload.findByID({
|
||||||
|
collection: 'exports',
|
||||||
|
id: doc.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(exportDoc.filename).toBeDefined()
|
||||||
|
const expectedPath = path.join(dirname, './uploads', exportDoc.filename as string)
|
||||||
|
const data = await readCSV(expectedPath)
|
||||||
|
|
||||||
|
// Assert that the csv file contains the expected virtual fields
|
||||||
|
expect(data[0].custom).toStrictEqual('my custom csv transformer toCSV')
|
||||||
|
expect(data[0].group_custom).toStrictEqual('my custom csv transformer toCSV')
|
||||||
|
expect(data[0].tabToCSV).toStrictEqual('my custom csv transformer toCSV')
|
||||||
|
expect(data[0].namedTab_tabToCSV).toStrictEqual('my custom csv transformer toCSV')
|
||||||
|
expect(data[0].customRelationship_id).toBeDefined()
|
||||||
|
expect(data[0].customRelationship_email).toBeDefined()
|
||||||
|
expect(data[0].customRelationship_createdAt).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
it('should create a JSON file for collection', async () => {
|
it('should create a JSON file for collection', async () => {
|
||||||
let doc = await payload.create({
|
let doc = await payload.create({
|
||||||
collection: 'exports',
|
collection: 'exports',
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ export interface Page {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
localized?: string | null;
|
localized?: string | null;
|
||||||
|
custom?: string | null;
|
||||||
|
customRelationship?: (string | null) | User;
|
||||||
group?: {
|
group?: {
|
||||||
value?: string | null;
|
value?: string | null;
|
||||||
ignore?: string | null;
|
ignore?: string | null;
|
||||||
@@ -161,6 +163,11 @@ export interface Page {
|
|||||||
id?: string | null;
|
id?: string | null;
|
||||||
}[]
|
}[]
|
||||||
| null;
|
| null;
|
||||||
|
custom?: string | null;
|
||||||
|
};
|
||||||
|
tabToCSV?: string | null;
|
||||||
|
namedTab?: {
|
||||||
|
tabToCSV?: string | null;
|
||||||
};
|
};
|
||||||
array?:
|
array?:
|
||||||
| {
|
| {
|
||||||
@@ -465,6 +472,8 @@ export interface UsersSelect<T extends boolean = true> {
|
|||||||
export interface PagesSelect<T extends boolean = true> {
|
export interface PagesSelect<T extends boolean = true> {
|
||||||
title?: T;
|
title?: T;
|
||||||
localized?: T;
|
localized?: T;
|
||||||
|
custom?: T;
|
||||||
|
customRelationship?: T;
|
||||||
group?:
|
group?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
@@ -477,6 +486,13 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
field2?: T;
|
field2?: T;
|
||||||
id?: T;
|
id?: T;
|
||||||
};
|
};
|
||||||
|
custom?: T;
|
||||||
|
};
|
||||||
|
tabToCSV?: T;
|
||||||
|
namedTab?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
tabToCSV?: T;
|
||||||
};
|
};
|
||||||
array?:
|
array?:
|
||||||
| T
|
| T
|
||||||
|
|||||||
@@ -91,6 +91,16 @@ export const seed = async (payload: Payload): Promise<boolean> => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
await payload.create({
|
||||||
|
collection: 'pages',
|
||||||
|
data: {
|
||||||
|
customRelationship: user.id,
|
||||||
|
title: `Custom ${i}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
await payload.create({
|
await payload.create({
|
||||||
collection: 'pages',
|
collection: 'pages',
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@payload-config": ["./test/_community/config.ts"],
|
"@payload-config": ["./test/plugin-import-export/config.ts"],
|
||||||
"@payloadcms/admin-bar": ["./packages/admin-bar/src"],
|
"@payloadcms/admin-bar": ["./packages/admin-bar/src"],
|
||||||
"@payloadcms/live-preview": ["./packages/live-preview/src"],
|
"@payloadcms/live-preview": ["./packages/live-preview/src"],
|
||||||
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
|
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
|
||||||
|
|||||||
Reference in New Issue
Block a user