Features Apograph CMS on GitHub

Export and import

Transfer plugin @apograph/transfer-server@apograph/transfer-admin

Moving content out of one Apograph and into another, without duplicating half of it.

Documents 0.5.2 Updated Edit this page Report a problem

On this page

New in 0.4.0. Export and import live in the Content Library’s own menus — there is no separate transfer screen, because a transfer is an action on content somebody is already looking at.

WhereWhat is there
The entry editor’s ⋯ menuExport…, for the record you have open
The selection bar’s ⋯ menuExport, for what you have ticked
The collection’s ⋯ menuImport…

Import belongs to the collection rather than the selection because there is nothing selected when you import — what arrives is whatever the file holds. It is hidden in the trash view, where the rows are on their way out.

What an export carries

The records you chose, plus one hop: the records they point at. An article’s author comes along so the link can be made again on the other side.

Depth-1 records still get resolvable references, filled from a key-only projection over the next layer out. A reference carrying only a foreign row id would be unresolvable anywhere else, which is the whole problem natural keys exist to solve.

The export dialog’s counts come from a preview that runs the same graph walk the export will. “Include related records” is the difference between 40 records and 4,000, and the count is the only warning you get.

Formats

FormatCarries filesLosslessGood for
JSONnoyesOne file, several types, human-readable
NDJSONnoyesStreaming, and diffing line by line
ZIPyesyesAnything with media attached
CSVnonoA spreadsheet somebody will edit

Only ZIP carries asset bytes. Pick anything else and the “include files” toggle disables itself rather than accepting a promise the server will not keep.

CSV is lossy on purpose, and predictably

Media cells are deliberately not reconstructed on the way back in. A filename in a spreadsheet is not a file, and inventing an asset reference from one relinks records to whatever happened to share a name.

Use CSV to get content out to somebody who wants a spreadsheet. Use JSON or ZIP to move content between installations.

How a record is matched on the way in

Not by row id. A row id names one database, so everything an import can do rests on a natural key — the field that identifies a record in a way that means something somewhere else.

A localized field is a candidate. A transfer record is one row, not one record across languages, so a per-locale slug identifies exactly the row it belongs to; the locale is folded into the key so en/hello and de/hello stay apart.

Matching runs in two passes, in this order:

  1. The natural key, which is the identity that means something on another installation.
  2. The source row id, and only when the key found nothing. A $id that names a live row of this type in this workspace is not a guess — it is the row the document was written from.

That second pass is what lets a type with no derivable identity field be linked to rather than copied on every import.

Name your identity fields

Left unconfigured, a type falls back to a derived guess: a field named like an identifier — slug, sku, email — then the first required text field. Usually right, reported in every export’s manifest, and still a guess.

TransferPlugin({ identity: { product: ['sku'] } }) is the setting that decides whether importing the same file twice updates records or duplicates them. See the transfer plugin.

Two questions, not one

The import dialog asks about the records in the file and the records they point at, separately.

QuestionGovernsDefault
If a record is already hereThe records the file is about
Records this file links toThe depth-1 records they point atLink

A single answer cannot serve both. An export carries an article’s author so the link can be made again, and “add a second copy of this article” must not quietly mean “and a second author”.

Link writes nothing to a related record that already exists. It exists so the link can be made, not so the row can be rewritten.

Verdicts before anything is written

Import is two-phase: a preview pass reports what each record in the file would do — created, updated, linked, skipped — and only then does the Import button do it.

Verdicts go stale deliberately. Changing the file, or either policy, retires the table: showing one file’s verdicts above another file’s Import button would be the worst bug this dialog could have.

After a run, every type the import touched is refreshed — the verdicts name them, so the set is exact. A run that creates an article and the author it points at refreshes both lists.

Limits

An export is bounded so a careless filter cannot ask one request to stream a whole library into memory. An import is bounded because the file is untrusted: a small archive can describe an enormous one, and the compression ratio is the signal that arrives before the bytes do.

The full table of ceilings, and how to override them, is on the transfer plugin.

What it guarantees

Every write goes through the same writer service the editor uses — field validation, the workspace scope, relation-target checks, the same-locale rule, the media-asset check, revisions, the audit log and the localization extension all run.

Nothing in an uploaded file names a workspace. The manifest carries the source workspace and the importer never reads it: the request’s workspace is stamped on every write.

Permissions

content:export and content:import, both granted to contributor as well as admin. Downloading an import template needs only content:read.