obsidian/wiki/payloadcms/plugin-import-export.md
2026-05-15 16:17:45 +01:00

261 lines
8.4 KiB
Markdown

---
title: "Import/Export Plugin"
aliases: [payload-import-export, plugin-import-export, csv-export-payload]
tags: [payloadcms, plugin, import, export, csv, json, etl, jobs-queue]
sources: [raw/plugins__import-export.md]
created: 2026-05-15
updated: 2026-05-15
---
# Import/Export Plugin
Adds CSV and JSON import/export to the Payload admin UI. Exports can be downloaded directly or saved as upload documents; imports support create/update/upsert modes.
## Installation
```bash
pnpm add @payloadcms/plugin-import-export
```
## Basic Setup
```ts
import { importExportPlugin } from '@payloadcms/plugin-import-export'
buildConfig({
plugins: [
importExportPlugin({
collections: [{ slug: 'users' }, { slug: 'pages' }],
}),
],
})
```
## Jobs Queue — Required
By default all operations run through [[wiki/payloadcms/jobs-queue|Payload's Jobs Queue]]. Without a runner, imports/exports stay `pending` forever.
```ts
jobs: {
autoRun: [{ cron: '*/5 * * * *', queue: 'default' }],
},
```
For small datasets or testing, bypass the queue per-collection:
```ts
{ slug: 'pages', export: { disableJobsQueue: true }, import: { disableJobsQueue: true } }
```
## Plugin-Level Options
| Property | Type | Description |
|----------|------|-------------|
| `collections` | array | Which collections to enable. Defaults to all. |
| `exportLimit` | `number \| fn` | Global max docs per export (`0` = unlimited). |
| `importLimit` | `number \| fn` | Global max docs per import (`0` = unlimited). |
| `overrideExportCollection` | function | Modify the shared `exports` upload collection (e.g. add access control). |
| `overrideImportCollection` | function | Modify the shared `imports` upload collection. |
| `debug` | boolean | Enable debug logging. |
## Per-Collection Config
```ts
collections: [
{
slug: 'pages',
export: {
format: 'csv', // force format
batchSize: 100, // docs per batch
limit: 1000, // override global exportLimit
disableDownload: true, // hide Download button
disableSave: false, // show Save button
hooks: { before, after },
},
import: {
defaultVersionStatus: 'draft',
batchSize: 100,
limit: 500,
hooks: { before, after },
},
},
{
slug: 'posts',
export: false, // disable export entirely
},
]
```
## Exporting
Four methods:
1. **Admin UI** — Download (streams directly) or Save (stores in `exports` collection)
2. **Local API**`payload.create({ collection: 'exports', data: { collectionSlug: 'pages', format: 'json' } })`
3. **Jobs Queue**`payload.jobs.queue({ task: 'createCollectionExport', input: ... })`
4. **REST**`POST /api/exports/download`
**Selection modes in UI:** all documents / current filters / checked selection.
**Export parameters:** `format`, `limit`, `page`, `sort`, `depth` (default 1), `locale`, `drafts`, `fields`, `where`, `filename`.
## Importing
Four methods:
1. **Admin UI** — Import drawer from collection list view
2. **Local API**`payload.create({ collection: 'imports', data: { collectionSlug, importMode }, file })`
3. **Jobs Queue**`payload.jobs.queue({ task: 'createCollectionImport', input: ... })`
4. **File storage** — upload to `imports` collection directly
**Import modes:**
| Mode | Behaviour |
|------|-----------|
| `create` | Create only — fails on duplicate IDs |
| `update` | Update only — requires `id` column, fails on missing IDs |
| `upsert` | Create or update — most flexible |
Match by non-ID field with `matchField`:
```ts
data: { collectionSlug: 'users', importMode: 'upsert', matchField: 'email' }
```
**Import result fields:** `status`, `summary.total`, `summary.imported`, `summary.updated`, `summary.issues`, `summary.issueDetails`.
## Hooks
Hooks fire **once per batch** for both CSV and JSON.
```ts
export: {
hooks: {
// Modify data before write — return modified array
before: ({ data, format, batchNumber, totalBatches, originalData, req }) => {
return data.map(row => { const { passwordHash: _, ...safe } = row; return safe })
},
// After write — return value ignored, use for logging
after: ({ batchNumber, totalBatches, data, format, req }) => { ... },
},
},
import: {
hooks: {
before: ({ data }) => data.map(doc => ({ ...doc, email: doc.email?.toLowerCase() })),
// ImportAfterHook receives `result` instead of `data`
after: ({ result, req }) => { req.payload.logger.info({ imported: result.imported }) },
},
},
```
**Execution order:** field-level hooks → collection-level hooks.
### Column Name Mapping (foreign systems)
```ts
// Export: rename Payload keys → foreign column names
before: ({ data }) => data.map(row => {
const map = { title: 'Post Title', excerpt: 'Summary' }
return Object.fromEntries(Object.entries(row).map(([k, v]) => [map[k] ?? k, v]))
}),
// Import: rename foreign columns → Payload field names
before: ({ data }) => data.map(doc => {
const map = { 'Post Title': 'title', 'Summary': 'excerpt' }
return Object.fromEntries(
Object.entries(doc).filter(([k]) => map[k]).map(([k, v]) => [map[k], v])
)
}),
```
## Field-Level Options
Configure via `custom['plugin-import-export']` on any field:
```ts
{
name: 'internalField',
type: 'text',
custom: {
'plugin-import-export': {
disabled: true, // exclude from all import/export
hooks: {
beforeExport: ({ value, siblingData, format }) => { ... },
beforeImport: ({ value, data, format }) => { ... },
},
},
},
}
```
- `beforeExport` — return value to replace field, `undefined` for default, or mutate `siblingData` to add extra columns
- `beforeImport` — return transformed value, `undefined` to skip, `null` to explicitly set null
- **Virtual fields:** exported (computed values), but skipped on import
## CSV Format Details
- Nested fields: `group_value`, `array_0_field`
- Blocks: `blocks_0_<blockSlug>_blockType`
- Localized: `title_en`, `title_es` (multi-locale with `locale: 'all'`)
- Relationship has-one: `fieldName` (monomorphic), `fieldName_relationTo` + `fieldName_id` (polymorphic)
- Relationship has-many: `fieldName_0`, `fieldName_1` (monomorphic)
- Auto-coercion: `"true"/"false"` → boolean, `"null"` → null, numeric strings → number
## Separate Export Collections per Data Type
Override the slug to create isolated upload collections with different access control:
```ts
{
slug: 'users',
export: {
overrideCollection: ({ collection }) => ({
...collection,
slug: 'user-exports',
access: { read: ({ req }) => req.user?.role === 'superadmin' },
}),
},
}
```
Collections sharing the default `exports`/`imports` slug are unaffected.
## Admin UI Visibility
`exports`/`imports` collections are hidden from nav by default. To show them:
```ts
overrideExportCollection: ({ collection }) => ({
...collection,
admin: { ...collection.admin, group: 'Data Management' },
}),
```
## Access Control Warning
Users with read access to the `exports` upload collection can download **all exported data**, bypassing per-document access control on the source collection. Always add access control via `overrideExportCollection`.
## Key Takeaways
- **Jobs Queue required by default** — configure `jobs.autoRun` or set `disableJobsQueue: true` for small/test datasets
- **Four export methods:** admin UI download, admin UI save, Local API, Jobs Queue
- **Three import modes:** `create` / `update` / `upsert`; use `matchField` to match by non-ID field
- **Hooks fire per batch**, not per document — return modified array in `before`, use `after` for logging
- **Column mapping via hooks:** `export.hooks.before` renames out, `import.hooks.before` renames in; field-level `beforeExport` is alternative for shared fields
- **Access control gap:** `exports` upload collection bypasses source-collection ACL — add guards via `overrideExportCollection`
- **Virtual fields:** included in exports (computed), skipped on import
- **CSV auto-coercion:** `"null"` → null, `"true"` → boolean, numeric strings → number; use `beforeImport` hook to preserve literal strings
## Related
- [[wiki/payloadcms/jobs-queue|Jobs Queue — Overview]]
- [[wiki/payloadcms/jobs-queue-queues|Jobs Queue — Queues & Execution]]
- [[wiki/payloadcms/upload|Upload & Media]]
- [[wiki/payloadcms/access-control|Access Control]]
- [[wiki/payloadcms/hooks-collections|Collection Hooks]]
- [[wiki/payloadcms/plugins|Official Plugins]]
## Sources
- `raw/plugins__import-export.md`
- https://payloadcms.com/docs/plugins/import-export