Skip to content

Imports and exports

A module does not write its own importer. The kernel provides one, and it enforces the thing nobody enforces on themselves: a dry run before any write.

Getting data out

Every list exports, with nothing for a module to declare:

Fenêtre de terminal
curl -H "Authorization: Bearer $TOKEN" \
"/api/v1/library_title/export?format=csv"
id,name,author,isbn,published_year,genre,summary,cover
1,Dune,Frank Herbert,9780441013593,1965,fiction,"A desert planet, a spice, and an empire...",8

Four formats: csv, json, jsonl, xml. The export goes through the gated session: a reader exports only what they may read, and a forbidden row is not “hidden”, it does not exist in the file.

Bookkeeping columns (version, created_by_id, deleted_at…) stay out. Not fussiness: the import this export feeds refuses them, so shipping them would break the loop. id stays, because it is how an edited row finds its record again.

To pick, and to fix the order:

Fenêtre de terminal
"/api/v1/library_title/export?format=csv&fields=isbn,name,genre"
isbn,name,genre
9780441013593,Dune,fiction

An unknown name is refused rather than ignored:

422 {"detail":"unknown export field(s): titel"}

A typo that exports an empty column looks exactly like a model that holds nothing.

Getting data in

An import is a plan you approve, not a button. Four states, and none is skipped:

analyzed → approved → (dry run) → executed

1. Upload. The file goes up, the kernel reads it and proposes a column → field mapping. mode=ai guesses it; mode=manual leaves it empty for you to fill.

Fenêtre de terminal
curl -X POST /api/v1/admin/imports \
-H "Authorization: Bearer $TOKEN" \
-F "file=@new_titles.csv" -F "mode=manual"

2. Correct the mapping, with a PATCH on the plan: each source column points at a field of the target model.

3. Approve. A plan that is only analyzed does not execute:

409 {"detail":"cannot move import plan from 'analyzed' to 'executed'"}

4. Dry run. This is the step that justifies all the rest:

Fenêtre de terminal
curl -X POST "/api/v1/admin/imports/$PLAN/execute?dry_run=true" \
-H "Authorization: Bearer $TOKEN"
{
"dry_run": true,
"sources": [{
"target_model": "library_title",
"created": 1,
"skipped": 0,
"errors": [{
"row": 1,
"error": "duplicate key value violates unique constraint \"library_title_isbn_key\"\nDETAIL: Key (isbn)=(9780062316097) already exists."
}]
}]
}

That report comes from a real run: the file’s second line carried an ISBN that already existed. The table did not move by a row. Fix the file and start again: the error is per row, with its number, so you know which one to take back.

5. Execute, the same call without dry_run.

Replaying does not duplicate

Every written row is registered under a hash of its content, in the external-id registry. Running the same file again:

0 created, 1 skipped

What concerns you as a module author

Almost nothing, which is the point. Three things only:

  • A non-nullable field with no default will make every incomplete row refused. That is correct, but say so in your onboarding rather than letting a customer find out on a 4,000-row file.
  • A relation is given by its name, not by an id: the kernel resolves the value against the target model and creates the missing record if needed. A genre or a category therefore imports in plain text.
  • A unique constraint is your friend: it is what stopped the dry run above. A model with no uniqueness accepts duplicates in silence, and nobody sees them before the first wrong report.