Skip to content

feat(plugin-import-export): give import/export hooks access to the triggering document - #17797

Draft
nathanlentz wants to merge 4 commits into
mainfrom
feat/import-export-hooks-doc-access
Draft

feat(plugin-import-export): give import/export hooks access to the triggering document#17797
nathanlentz wants to merge 4 commits into
mainfrom
feat/import-export-hooks-doc-access

Conversation

@nathanlentz

@nathanlentz nathanlentz commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

What

Gives the collection-level import/export hooks access to the document that triggered the run, as exportDoc and importDoc. This lets a hook read a value an editor set on the import/export form — including a field the project added with overrideCollection — and apply it to every row.

importExportPlugin({
  collections: [
    {
      slug: 'posts',
      export: {
        overrideCollection: ({ collection }) => {
          collection.fields = [...collection.fields, { name: 'batchRef', type: 'text' }]
          return collection
        },
        hooks: {
          before: ({ data, exportDoc }) =>
            data.map((row) => ({ ...row, batchRef: exportDoc.batchRef })),
        },
      },
    },
  ],
})

How

The document is read once when the run starts and passed unchanged to every batch, so hooks see a snapshot from when the operation was triggered rather than live state. A large export can fire the hook hundreds of times, so re-reading per batch would be both wasteful and inconsistent mid-run.

Where the document comes from depends on the path:

Path Source Has id
Export via jobs queue findByID in the task handler yes
Import, synchronous and jobs queue the saved document both callers already hold yes
Export, synchronous submitted form data, in beforeOperation no
Export download submitted request body no
Export and import preview submitted request body no

id is therefore optional, and an id means the document is saved. To keep that rule true rather than merely conventional, the paths without a saved document run the incoming body through getSubmittedFormValues, which drops id, createdAt and updatedAt. A caller cannot put an id on a download or preview request and have a hook treat the run as saved.

The two preview components previously built their request bodies from a fixed list of built-in fields, so a custom field never reached the endpoint. Both now forward the whole form. A value-based signature of the form state is added to the debounce dependencies, so editing a custom field refreshes the preview once the editor stops typing rather than on every keystroke.

Tests

  • Integration coverage for before and after on every path: synchronous and queued import, queued and synchronous export, and download.
  • Document identity across batches, for both import and export.
  • Two tests where the form value actually changes what gets written — a stamped column on export, a modified title on import.
  • Two tests that a submitted id cannot make an unsaved run look saved.
  • Two Playwright tests that drive the real admin forms. These are the only tests that can catch a hand-built request body, since an integration test builds its own.

A posts-with-hooks-jobs collection was added so both the synchronous and task-handler paths are exercised; its plugin config leaves the jobs queue enabled.

Open item

Import and export previews also render on a saved operation document's edit view, where useDocumentInfo() has an id. The clients do not send it, so hooks previewing a saved document still receive an unsaved snapshot. Fixing it means sending the route ID and fetching the document server-side, and needs a decision on what happens to the open form's unsaved edits — merging them keeps the preview live but means an id no longer implies every value was validated. Left out of this PR deliberately.

…iggering document

The collection-level import/export hooks received no reference to the document
that started the run, so a field added to the imports/exports collection via
`overrideCollection` could not be read while processing. That also left no way
to correlate a running job with its document in jobs mode.

Adds `importDoc` to `ImportBeforeHook`/`ImportAfterHook` and `exportDoc` to
`ExportBeforeHook`/`ExportAfterHook`, covering all seven hook contexts:

- import jobs and sync, which already held the saved document
- export jobs, which now reads the document back — the job input schema is a
  fixed whitelist, so fields added via `overrideCollection` do not survive
  serialization
- export sync, download, and both preview endpoints, which pass the submitted
  form data

`id` is optional on both types because only the export jobs path has a saved
document when the hooks run. Export sync runs in `beforeOperation`, and the
download and preview paths never persist one. On those, every user-authored
field is present but `id`, `createdAt` and `updatedAt` are not, and the values
are unvalidated request input — both documented.

The document is read once when the run starts and passed unchanged to every
batch, so it is a snapshot rather than live state. A long run would otherwise
add a database read per batch to observe a change nothing needs.

Closes #16961
…w endpoints

The preview components built their request bodies from a fixed list of built-in
fields, so a field added to an imports/exports collection via
`overrideCollection` never reached the collection-level hooks. Both now forward
the whole form, and re-fetch when a custom value changes.

The endpoints drop `id`, `createdAt` and `updatedAt` from the incoming body, so
an `id` on `exportDoc`/`importDoc` means the document really is saved rather
than something a caller can put on a request.

Adds coverage for the queued-import, synchronous-export and download after
hooks, for document identity across batches, and for two cases where a form
value changes the rows that get written. The new tests clean up the import and
export documents and the job records they create.
…ming

Cleanup no longer catches deletion failures, so a delete that does not succeed
fails the test rather than passing quietly. Job records are tracked per test: a
snapshot of the existing payload-jobs IDs is taken before each test and only the
records added on top of it are removed, instead of deleting every job record.

Splits the download test so each one checks a single hook, routes the repeated
post-and-export setup through helpers, and renames `withBatchRefField` and
`generatedKeys` to describe what they actually do.
@github-actions

Copy link
Copy Markdown
Contributor

📦 esbuild Bundle Analysis for payload

This analysis was generated by esbuild-bundle-analyzer. 🤖
This PR introduced no changes to the esbuild bundle! 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant