Bulk load and exports
The API calls behind bulk load and exports.
On this page
Part of the HTTP API reference.
Route: #/load or #/load/<CODE> → drawLoad(code). The type
code is upper-cased; the route highlights LOAD in the sidebar rather than the
type's own list, because "you are on the load screen, not on that type's list,
and highlighting the list is a lie about where you are".
The one rule that is deliberately the opposite of the other two workbooks:
Same download-edit-preview-apply shape as the other two, and one rule that is deliberately the opposite of theirs: an asset missing from the workbook is LEFT ALONE. Removing something takes an X in its Delete column. The other two are a complete statement of a small model, so absence there means deletion. This is a partial statement about a large one, and absence meaning deletion would turn "load fifty new terms" into "destroy the other four hundred".
The order of calls
GET /api/bulk— which types can be loaded, and their shape.GET /api/bulk/export?empty=1[&type=<CODE>]as an<a href>, or?type=<CODE>from the type list's export box.POST /api/bulk/previewon choosing a file.POST /api/bulk/applyon pressing Apply.
GET /api/bulk — which types can be loaded
No parameters, no permission check.
Docstring: "Which types can be loaded this way, and how many of each there are."
Authored types only (origin = 'authored').
Response
{
"types": [ // ORDER BY display_order, code
{"code": "BUSINESS_TERM", "name": "Business Term",
"plural": "Business Terms", "description": "…",
"assets": 412, // proposals excluded
"attributes": 7, // how many type_attribute rows
"mandatory": 2,
"columns": [ // THE COLUMNS THE SHEET WILL HAVE
{"name": "Definition", "data_type": "text",
"mandatory": true, "allowed": null}
]}
],
"harvested_types": 11 // how many types cannot be loaded this way
}
columns excludes harvested attributes and is "Small enough to send with the
list, and it lets the screen show what you are about to download rather than
only how many of it."
GET /api/bulk/export — a workbook to fill in
No permission check.
Query parameters
| Name | Type | Default | Notes |
|---|---|---|---|
type |
string | null |
stripped, upper-cased; one type only |
empty |
bool | false |
true gives a blank template |
Docstring: "A workbook to fill in — populated with what is there, or blank."
Refusal: 400 with bulk_io.from_database's ValueError — e.g. naming a
harvested type.
Response: xlsx, filename
krisid-<type-slug|assets>-<YYYY-MM-DD>.xlsx, with a scope line inside
reading either "<Plural> only" or "All authored asset types", and
" · empty template" appended when empty.
POST /api/bulk/preview
multipart/form-data with file. No permission check. Always rolled
back.
Refusal: 400 with bulk_io.parse's ValueError.
Response: {"filename": …, **p.as_dict, "workflow_gate": "direct"}.
workflow_gate is direct, hold or reject — what workflow_bulk_loads
says for this person, allowing for WORKFLOW_BYPASS_LOAD. The client shows
it before Apply, not after: "Where workflows reach into loads,
say so before Apply, not after."
workflow_gate |
The sentence the client shows |
|---|---|
hold |
"Changes to assets a workflow governs will be held as change requests rather than written; everything else is written." |
reject |
"Rows for assets a workflow governs will be left out, and named; everything else is written." |
POST /api/bulk/apply
Permission: ASSET_BULK_LOAD, over the whole repository.
multipart/form-data with file.
Docstring — this one is a fixed security hole, said plainly:
It had no check at all: the starter model gave the permission to two roles and nothing asked for it, so anybody signed in — a Viewer included — could rewrite every authored asset from a spreadsheet. And the name recorded against the changes came from the query string, so it could be anybody's. Now it is the person signed in, and they must hold the permission.
Note the contrast: /api/metamodel/apply and /api/access/apply still take an
actor query parameter and no permission check at all; all three are guarded
now, and none of them takes a caller-supplied actor.
Refusal: 400 with bulk_io.parse's ValueError.
Response
{
"filename": "terms.xlsx",
"applied": true,
"…": "the plan shape",
"workflow": { // null when the gate was `direct`
"mode": "hold",
"held": ["BUSINESS_TERM NET_REV: 2 change(s) proposed",
"link BUSINESS_TERM NET_REV → DATA_DOMAIN FINANCE: proposed",
"BUSINESS_TERM CHURN: deletion proposed"],
"rejected": ["BUSINESS_TERM GROSS_REV: governed by a workflow"]
}
}
What hold and reject do, row by row (_governed_write):
| Row | hold |
reject |
|---|---|---|
| a change to a governed existing asset | one change request per asset, carrying the field-by-field diff | left out and named |
| a new asset that turns out to be governed | made, then kept as a proposal with a create request |
made, then deleted again |
| a new asset nothing governs | proposed_by cleared; written as normal |
the same |
| a link where either end is governed | a relation_add item on the governed end's request, or on the far end if the near one is not governed |
left out and named |
| a link to a new asset that is still a proposal | added to that asset's own create request |
left out |
| a deletion of a governed asset | a deletion request via _fire("deleted", …) |
left out and named |
New assets are "made, then asked — a scope may decide on the values just loaded."
On the refusal log: when the plan is not safe the transaction is rolled back, and the record of the refusal is written on a second connection — "A refusal nobody can see afterwards is how 'I uploaded it and nothing happened' becomes unanswerable."