feat(plugin-import-export): add support for collection-level and field-level hooks#16252
feat(plugin-import-export): add support for collection-level and field-level hooks#16252
Conversation
📦 esbuild Bundle Analysis for payloadThis analysis was generated by esbuild-bundle-analyzer. 🤖
Largest pathsThese visualization shows top 20 largest paths in the bundle.Meta file: packages/next/meta_index.json, Out file: esbuild/index.js
Meta file: packages/payload/meta_index.json, Out file: esbuild/index.js
Meta file: packages/payload/meta_shared.json, Out file: esbuild/exports/shared.js
Meta file: packages/richtext-lexical/meta_client.json, Out file: esbuild/exports/client_optimized/index.js
Meta file: packages/ui/meta_client.json, Out file: esbuild/exports/client_optimized/index.js
Meta file: packages/ui/meta_shared.json, Out file: esbuild/exports/shared_optimized/index.js
DetailsNext to the size is how much the size has increased or decreased compared with the base branch of this PR.
|
DanRibbens
left a comment
There was a problem hiding this comment.
I need help understanding this. I know the API of toCSV is messy, but it is very contextually easy to work with when you have a field configured in code that gets shared across collections for example. Also writing hooks at the collection level for deeply nested fields is a PITA to work with.
| try { | ||
| const transformed = hook({ | ||
| columnName: fieldKey, | ||
| data: doc, |
There was a problem hiding this comment.
data receives the current-scope doc, not the top-level document, right?
The type JSDoc says data is "The full flat row (CSV) or document (JSON) being imported", but for fields nested inside groups, arrays, or blocks, doc here is the narrowed sibling object at that level. A hook doing data[someTopLevelKey] will silently get undefined.
The export hook solves this by threading topDoc through every recursive call. Same fix needed here: add topDoc to ApplyArgs, initialize it as doc in applyFieldBeforeImportHooks, pass it down through all recursive calls, and change data: doc → data: topDoc in the hook invocation.
There was a problem hiding this comment.
excerpt, hasManyNumber, hasOnePolymorphic_id seem like test collection field names, why are we logging these here?
| field.custom?.['plugin-import-export']?.toCSV | ||
|
|
||
| if (typeof fieldExportHook === 'function') { | ||
| result[fullKey] = fieldExportHook as FieldBeforeExportHook |
There was a problem hiding this comment.
Field hooks on block fields silently don't fire (CSV or JSON)
getExportFieldFunctions uses traverseFields to register user hooks under a static, index-free key — e.g. content_textBlock_text. At lookup time, flattenObject (CSV) constructs content_0_textBlock_text and falls back to text; applyFieldBeforeExportHooks (JSON) tries content_0_textBlock_text with no fallback at all. Neither path ever hits content_textBlock_text, so the hook is silently skipped.
Default handlers dodge this because they're also registered under baseKey = field.name (matching the text fallback), but user-defined hooks get no such alias.
Fix: strip runtime indices from the lookup key and try it as a secondary fallback in both flattenObject and applyFieldBeforeExportHooks.
Why
Field-level hooks are essential because:
group.tabs.namedTab.array[0].fieldin a collection-level hook requires manually navigating the full document structure; field-level hooks handle this transparentlyThis PR keeps both levels: field-level hooks for per-field transforms, collection-level hooks for batch-wide operations (masking, logging, filtering).
Field-level hooks
Defined per-field via
custom['plugin-import-export'].hooks:Collection-level hooks
Defined in the plugin config:
Execution order
field-level
hooks.beforeExport/hooks.beforeImportrun first (per-field, per-document), then collection-levelhooks.before/hooks.afterrun on the already-transformed batch.Migration from toCSV / fromCSV
toCSV and fromCSV remain fully functional but are deprecated — removed in v4.0. Migration is a 1:1 rename plus the new format parameter:
What changed
New types
FieldBeforeExportHook— field-level export hook type (same args as ToCSVFunction + format)FieldBeforeImportHook— field-level import hook type (same args as FromCSVFunction + format)ToCSVFunction/FromCSVFunction— now deprecated aliasesJSON format support (new)
Bug fixes
Renamed internals
Checklist