---
title: "Import and export reference plugin"
image: "https://adonia.pages.dev/og.png"
version: "next"
---

> Documentation Index
> Fetch the complete documentation index at: https://adonia.pages.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Import and export reference plugin

`adonia-import-export` provides streaming CSV/XLSX codecs, scope-bound import orchestration, durable import history, reusable export presets, private Drive/S3 artifacts, and owned background transfers. Its installable manifest contributes guarded panel routes, worker action names, migrations, and a doctor check; the host still supplies authenticated controller and worker bindings because authorization, resource queries, and write policy are application-owned.

## Install

```sh
pnpm add adonia-import-export @adonisjs/lucid
```

Large background transfers also require an `AdoniaQueueContract`. Install the supported BullMQ adapter when chosen, and install Drive for S3/GCS/private object storage:

```sh
pnpm add @adonia/queue-bullmq bullmq @adonisjs/drive
```

## Declare an allowlist

Every imported or exported attribute must be in `static importExportColumns`. The server plugin validates this declaration at resource boot.

```ts
import { BaseResource } from '@adonia/core'
import { defineTransferColumns } from 'adonia-import-export'
import Post from '#models/post'

export default class PostResource extends BaseResource<typeof Post> {
  static override model = Post
  static override slug = 'posts'

  static importExportColumns = defineTransferColumns([
{
  key: 'title',
  label: 'Title',
  required: true,
  aliases: ['Post title'],
},
{
  key: 'readingMinutes',
  label: 'Reading minutes',
  parse: (cell: string) => Number(cell),
},
  ])

  override schema() {
return []
  }
}
```

Mapping suggestions are case-insensitive hints only. The user must confirm a `ColumnMapping[]`; `resolveColumnMapping` rejects duplicate headings, targets outside the allowlist, non-importable columns, and missing required mappings. `ImportService` persists the complete server-owned panel/actor/tenant/parent identity on every source, rechecks exact equality during confirmation, validates bounded rows, and never sends unallowlisted cells to a writer.

Attach the installable server manifest. It validates resource allowlists and registers the package routes, workers, migrations, and doctor check:

```ts
import { Panel } from '@adonia/core'
import { importExportPlugin } from 'adonia-import-export'

export const admin = Panel.make('admin').plugins([importExportPlugin()])
```

Install the mapping field in the UI registry:

```ts
import { importExportUiPlugin } from 'adonia-import-export/client'
import { adonia } from '@adonia/ui'

export default adonia({
  registry(registry) {
registry.use(importExportUiPlugin)
  },
})
```

`ImportWorkflow` is a controlled upload → mapping → confirmation component. Its `upload` and `confirm` callbacks must call authenticated host routes under the panel middleware stack.

## Import service

Construct `ImportService` with an `ImportSourceStore`, a server-derived `scope`, a resource-to-column resolver, and `writeRows`. `FileImportSourceStore` is the private shared-volume implementation. For horizontally scaled deployments, combine `DriveSourceStore` with `LucidSourceRecordStore`; bytes live on the configured private Drive disk (including S3), while scope and expiry metadata live in the database.

`confirm({ dryRun: true })` validates every row, resolves upsert matches, returns row errors plus `wouldCreate`/`wouldUpdate`, invokes no writer, and retains the uploaded source so the same mapping can be committed afterward. Upsert mode requires mapped `matchOn` key(s), `matchRows`, and—outside preview—`writeUpserts`; `conflictPolicy: 'skip' | 'update'` controls matched rows. When `artifacts` is configured, a non-preview import with rejected rows returns a signed failure CSV containing the original row, source row number, worksheet identity when applicable, and validation errors.

Imports default to at most 100,000 data rows, 50 columns, and one worksheet. `maxRows`, `maxColumns`, `maxWorksheets`, upload byte, cell, row, error, and batch options may tighten those limits. The host writers still own transactions, model hooks, tenant stamping, and authorization.

## Resource action factories

Actions remain explicit resource declarations; installing the manifest does not silently add actions to resources. `createImportAction` confirms an opaque source prepared by an authenticated upload route, and `createExportAction` queues only rows already authorized by Adonia. Both factories reject malformed form state before calling their configured services.

The factories have deliberately different jobs:

- `createImportAction({ service, resource?, slug?, label?, icon?, contexts? })` creates a synchronous **page** action. It accepts an opaque `sourceId` plus the confirmed mapping and calls the scope-bound `ImportService.confirm`.
- `createExportAction({ service, columns, identity, resource?, statusUrl?, slug?, label?, icon?, contexts? })` creates **row and bulk** actions. `columns` is the server-owned export allowlist, `identity` derives panel/actor/tenant/lens/parent from the authorized action context, and `statusUrl` may point at the owned progress page.
- `importExportPlugin()` returns the installable manifest. Host container bindings implement the contributed HTTP and worker boundaries; routes never bypass the panel middleware or host authorization.

```ts
import { BaseResource, type AnyAction } from '@adonia/core'
import {
  createExportAction,
  createImportAction,
  defineTransferColumns,
  type ExportActionOptions,
  type ImportService,
  type QueuedExportService,
} from 'adonia-import-export'

type Post = { id: number; title: string }
declare const imports: ImportService<Post>
declare const exports: QueuedExportService
declare const exportIdentity: ExportActionOptions<Post>['identity']

class PostResource extends BaseResource {
  static override slug = 'posts'
  static importExportColumns = defineTransferColumns<Post>([
{ key: 'id', label: 'ID', importable: false },
{ key: 'title', label: 'Title', required: true },
  ])

  override schema() {
return []
  }

  override actions(): AnyAction[] {
return [
  createImportAction({ service: imports, resource: 'posts' }),
  createExportAction({
    service: exports,
    resource: 'posts',
    columns: PostResource.importExportColumns,
    identity: exportIdentity,
  }),
]
  }
}
```

The import action modal accepts the `sourceId` and confirmed `ColumnMapping[]` returned by the host upload/preparation flow. The export modal accepts format, safe filename, and an explicit subset of the server-owned export allowlist. Supply `statusUrl` when the host has an authenticated progress page and wants the action result to open it.

## Manifest routes and host bindings

The manifest contributes panel-scoped named routes. Each handler resolves the host's `adonia.importExportController` binding; it does not invent authentication or database policy. The worker actions resolve `adonia.importExportWorker`. Missing bindings fail the plugin doctor check rather than degrading to unscoped access.

| Contributed route | Required host behavior |
| --- | --- |
| `POST transfers/:resource/import/upload` | Authorize import, derive panel/actor/tenant/parent, construct the scope-bound service, stream the upload, and return `ImportPreparation`. |
| `POST transfers/:resource/import/confirm` | Re-authorize and call `ImportService.confirm`; the service independently checks the persisted source scope. Set `dryRun` for preview. |
| `GET transfers/:resource/imports` | Return `QueuedImportService.history` for the exact current scope. |
| `POST transfers/:resource/imports/:id/retry` | Upload the corrected failure CSV, then call `retryFromFailures` with the new opaque source. |
| `GET/DELETE transfers/:resource/exports/:id` | Return owned status or request cooperative cancellation. Translate a missing/mismatched job to 404. |
| `GET .../download` / `POST .../regenerate` | Require a finished owned job; redirect to its signed URL or issue a new short-lived URL from the persisted artifact key. |
| `GET/POST/DELETE transfers/:resource/export-presets` | List, save, or delete named column/filter sets inside the exact owner scope. |
| `POST transfers/:resource/export-presets/:id/run` | Call `enqueuePreset`; validate and reapply the persisted opaque filter state in the scoped worker row loader. |

`ImportExportControllerContract<DownloadResponse>` and the endpoint DTOs type these host adapters without coupling the package's storage/services to Adonis HTTP response classes.

## Background imports and exports

`QueuedExportService` persists and dispatches exports. `savePreset` stores an owned descriptor-column/filter selection, and `enqueuePreset` replays both into a new job; filter data remains opaque and must be validated by the application row loader. `QueuedExportWorker` replays the complete identity, resolves descriptor-owned columns, enforces the configured row/column caps, streams CSV or XLSX, optionally gzip-compresses the artifact, and persists the stable object key separately from its expiring signed URL. Configure the service with the same artifact store to use `regenerateDownload`.

`QueuedImportService` persists a scope-bound confirmation and dispatches `IMPORT_ACTION`. `QueuedImportWorker` rejects payload/history identity drift before resolving the host's scope-bound `ImportService`, cooperatively checks cancellation, records progress, and stores the result/failure artifact. `history` is owner-scoped; `retryFromFailures` creates a linked history entry using a newly uploaded corrected failure CSV.

Use `FileImportJobRepository` for a shared private volume or `LucidImportJobRepository` for multi-host history and atomic claims. `FileExportArtifactStore` signs local opaque URLs; `DriveArtifactStore` delegates private streaming writes and signed downloads to `@adonisjs/drive`, so S3/GCS drivers work without buffering whole artifacts. `FileExportPresetRepository` and `LucidExportPresetRepository` persist named column/filter sets.

```ts
import type {
  ActionBatchStore,
  ActionWorkerRuntime,
  AdoniaQueueContract,
} from '@adonia/core'
import {
  IMPORT_ACTION,
  IMPORT_EXPORT_ACTION,
  QueuedExportService,
  QueuedExportWorker,
  QueuedImportService,
  QueuedImportWorker,
  type ExportArtifactStore,
  type ExportJobRepository,
  type ImportJobRepository,
  type ImportService,
} from 'adonia-import-export'

declare const queue: AdoniaQueueContract
declare const exportJobs: ExportJobRepository
declare const importJobs: ImportJobRepository
declare const artifacts: ExportArtifactStore
declare const batches: ActionBatchStore
declare const importsForJob: () => ImportService<{ title: string }>

export const exports = new QueuedExportService({
  queue,
  jobs: exportJobs,
  batches,
  artifacts,
})
export const imports = new QueuedImportService({
  queue,
  jobs: importJobs,
  batches,
})
const exportWorker = new QueuedExportWorker({
  jobs: exportJobs,
  artifacts,
  columns: () => [{ key: 'title', label: 'Title' }],
  async *loadRows() {},
})
const importWorker = new QueuedImportWorker({
  jobs: importJobs,
  service: importsForJob,
})

export const transferWorker: ActionWorkerRuntime = {
  work(job) {
if (job.action === IMPORT_EXPORT_ACTION) return exportWorker.work(job)
if (job.action === IMPORT_ACTION) return importWorker.work(job)
throw new Error(`Unsupported transfer action: ${job.action}`)
  },
}
```

Web and worker processes must share the queue, repositories, Drive disk, and resource declarations. `progressEvery` must be a positive integer; zero, negative, and fractional values throw `E_ADONIA_INVALID_CONFIG` at construction. Schedule source/artifact cleanup according to retention policy.

## Fail-closed tenant and nested-resource pattern

Never accept panel, resource, actor, tenant, lens, parent, target IDs, or selected columns from standalone client fields. Resolve them from the authenticated request/action context. Every `ImportSourceRecord` contains panel, actor, tenant, optional parent, and resource; `ImportService.confirm` compares all of them to its server-owned scope before reading rows. Treat a missing record or any mismatch as not found.

The worker must independently reject unknown resources and reapply every serialized scope before reading rows. Start from an empty/denied query, require the current tenant and parent, re-authorize the action for the persisted actor if the application supports worker-side policy reconstruction, and apply `ActionTargets` only after those constraints. Never fall back to an unscoped model query when tenant or parent resolution fails.

```ts
import type {
  ImportEndpointScope,
  QueuedExportIdentity,
} from 'adonia-import-export'

export function requireNestedScope(
  scope: ImportEndpointScope | QueuedExportIdentity
): { tenantId: string; parentId: string | number } {
  if (
typeof scope.tenant !== 'object'
|| scope.tenant === null
|| Array.isArray(scope.tenant)
|| typeof scope.tenant.id !== 'string'
|| scope.parent === undefined
  ) {
throw new Error('Scoped resource not found')
  }
  return { tenantId: scope.tenant.id, parentId: scope.parent.id }
}
```

## Security checklist

- Authorize resource/action access before upload, confirmation, enqueue, history, retry, preset mutation, cancellation, and download.
- Reapply panel, tenant, parent, lens, soft-delete, and selection/query scopes in every worker loader/writer.
- Keep sources and artifacts private; expose only short-lived signed downloads and regenerate them from an owned persisted key.
- Treat filenames, spreadsheet cells, formulas, queue payloads, persisted job state, mappings, match keys, and filters as untrusted.
- Run expiry/retention cleanup and avoid storing sensitive values in safe validation messages or failure CSVs.

See [`adonia-import-export` API](/reference/api/import-export) for the complete server surface and its `./client` subpath.

Source: https://adonia.pages.dev/reference/plugins/import-export/index.mdx
