Two surfaces: the REST contract (what build_crud_router serves and what
the clean-room frontend store consumes) and the Python API (the importable
symbols).
This is the contract the frontend createCrudStore was authored against. For a
spec with resource = "<resource>" and primary key <pk>, the router mounts at
/<resource>.
Query params:
| Param | Type | Default | Meaning |
|---|---|---|---|
limit |
int (1–200) | 50 | page size |
cursor |
string | — | opaque keyset token from a prior response's next_cursor; omit for the first page |
filter |
string | — | URL-encoded JSON array of filter specs (see below) |
A filter spec is {"field": <col>, "op": <op>, "value": <v>}. op is one of:
eq, neq, lt, lte, gt, gte, like, ilike, contains, in, nin,
is_null, not_null. in/nin take a non-empty list value;
is_null/not_null take no value. A field outside the entity's filterable
allow-list → 400.
Response 200:
{ "<resource>": [ { /* row */ }, ... ], "next_cursor": "OPAQUE_OR_NULL" }The list key is the resource name. next_cursor is null when there are no
more pages. Ordering is (created_at DESC, <pk> DESC).
200 → the row object. 404 when the row does not exist or is hidden by
the caller's tenant scope (deliberately indistinguishable).
Body: a JSON object of column values. Unknown columns → 400; a missing
required column → 400. 201 → the created row (with server-assigned id and
timestamps). Emits one create audit event.
Body: a JSON object of the fields to change. Only provided keys are updated; a
non-patchable key → 400; an empty body → 400. 200 → the updated row.
404 when absent / tenant-hidden. Emits one update audit event with a
(before, after) diff.
200 → { "deleted": true, "<pk>": "<id>" }. 404 when absent / tenant-hidden.
Emits one delete audit event.
Body:
{ "operation": "update" | "set_field" | "delete",
"ids": ["...", "..."],
"patch": { "col": "val" } }patch is required for update/set_field, ignored for delete. Up to 500
ids; a larger list or a patch key outside the allow-list → 400. 200:
{ "operation": "update", "affected": 5, "ids": ["...", "..."] }The whole batch runs in one transaction and emits exactly one audit row
(bulk_update / bulk_set_field / bulk_delete).
By default the router reads the tenant id from an X-Tenant-Id header
(demo-grade). In production you pass an auth_dependency that yields the
request's session and a tenant_resolver that maps it to a tenant id; an
unresolvable auth → 401.
All public symbols import from the top-level package:
from vue_pinia_crud_store import (
EntitySpec, CrudHandlers, build_crud_router, NotFound,
CrudStore, InMemoryStore, ListPage,
FilterSpec, compile_filters, SUPPORTED_OPS,
encode_cursor, decode_cursor,
BulkRequest, BulkResponse, BulkStatement, BulkError,
build_bulk_statement, affected_from_status, make_bulk_response, MAX_BULK_IDS,
WIDGET_SPEC, EVENT_SPEC,
)EntitySpec(
resource, table_name, pk_column="id",
columns=(), filterable_columns=None, patchable_columns=None,
required_create_columns=(), jsonb_columns=(), has_updated_at=True,
)filterable_columns defaults to all columns; patchable_columns defaults to
columns minus {pk_column, "created_at", "updated_at"}.
Framework-free async CRUD logic. CrudHandlers(spec, store, *, audit_emitter=None, id_validator=None).
| Method | Returns | Notes |
|---|---|---|
list(tenant_id, *, limit=50, cursor=None, filters=None) |
{<resource>: [...], "next_cursor": ...} |
filters is a list of spec dicts. Unknown column → ValueError. |
get(tenant_id, row_id) |
row dict |
NotFound when absent / hidden. |
create(tenant_id, data) |
created row dict |
Unknown / missing-required column → ValueError. Emits a create audit event. |
patch(tenant_id, row_id, patch) |
updated row dict |
NotFound / ValueError. Emits an update audit event. |
delete(tenant_id, row_id) |
{"deleted": True, <pk>: ...} |
NotFound. Emits a delete audit event. |
bulk(tenant_id, body) |
{"operation", "affected", "ids"} |
body is a bulk request dict. BulkError on bad input. One audit event. |
build_crud_router(
spec, store, *,
tenant_resolver, audit_emitter=None, id_validator=None,
auth_dependency=None, tags=None,
) -> fastapi.APIRouterRaises RuntimeError if FastAPI is not installed (the rest of the package works
without it).
CrudStore is the seam interface (async list_rows / get_row / create_row /
patch_row / delete_row / bulk). InMemoryStore(pk_column="id") is the
dependency-free default. ListPage(rows, next_cursor) is the list_rows return.
| Symbol | Description |
|---|---|
FilterSpec(field, op, value=None) |
A dataclass; validates op against SUPPORTED_OPS at construction. FilterSpec.from_dict(d) builds one from a dict. |
compile_filters(filters, allowed_columns, *, table_alias, start_param=1) |
→ (sql_fragment, params). Empty → ("TRUE", []). Raises ValueError on a non-allow-listed column, a bad in/nin value, or a bad alias. |
SUPPORTED_OPS |
The tuple of valid operator strings. |
| Symbol | Description |
|---|---|
encode_cursor(ts, row_id) |
→ opaque URL-safe base64 token (padding stripped). Naive datetimes are normalized to UTC. |
decode_cursor(token) |
→ (ts, row_id) (aware UTC datetime + string id) or None on every failure path (None/empty/bad-base64/bad-json/missing-keys/bad-timestamp). Never raises. |
| Symbol | Description |
|---|---|
BulkRequest(operation, ids, patch=None) |
Validates the operation and the id-count cap (MAX_BULK_IDS = 500) at construction. from_dict(d) builds one. |
build_bulk_statement(body, *, table_name, pk_column, patchable_columns, has_updated_at=True, id_validator=None) |
→ BulkStatement(sql, params, audit_op, diff). Raises BulkError on a bad id / patch. |
affected_from_status(status_str, prefix) |
Parse a driver command tag ("UPDATE 5") → row count (0 on anything unexpected). |
make_bulk_response(body, affected) |
→ BulkResponse(operation, affected, ids); .to_dict() for the JSON body. |
| Symbol | Maps to | Raised when |
|---|---|---|
NotFound |
HTTP 404 | a row is absent or hidden by tenant scope |
BulkError (a ValueError) |
HTTP 400 | a malformed bulk request (bad id, empty/invalid patch, key outside allow-list) |
WIDGET_SPEC — a generic mutable entity (has updated_at). EVENT_SPEC — a
generic append-only entity (has_updated_at=False). Both are domain-neutral
illustrations; define your own (see FORKING.md).