Docs › Reference

For everyone

Bulk load and exports

The API calls behind bulk load and exports.

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

  1. GET /api/bulk — which types can be loaded, and their shape.
  2. GET /api/bulk/export?empty=1[&type=<CODE>] as an <a href>, or ?type=<CODE> from the type list's export box.
  3. POST /api/bulk/preview on choosing a file.
  4. POST /api/bulk/apply on 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."