You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
vue-pinia-crud-store is a two-sided CRUD pattern. The Python half compiles an
EntitySpec into a REST surface over an injectable storage seam; the JavaScript
half is an independent Pinia store factory that consumes the documented REST
contract. The two halves share a contract, not a codebase — the backend can be
forked without the frontend and vice versa.
Module map (backend, src/vue_pinia_crud_store/)
Module
Responsibility
pagination.py
Keyset (cursor) pagination. encode_cursor(ts, id) → opaque URL-safe base64 token; decode_cursor(token) → (ts, id) or None on every failure path (a tampered cursor can never crash a handler). Pure stdlib.
filters.py
FilterSpec (a stdlib dataclass validating its op at construction) + compile_filters(...) → a parameterized WHERE-fragment + positional params. Three injection defenses: value parameterization, a column allow-list, and a table-alias identifier regex. Pure stdlib.
bulk.py
BulkRequest / build_bulk_statement(...) → a BulkStatement (SQL + params + audit-op + diff) for update / set_field / delete, capped at 500 ids; affected_from_status parses the driver's command tag. Pure stdlib.
store.py
CrudStore — the storage seam (a small set of async methods). InMemoryStore — the default, dependency-free implementation that replays the same filter / cursor / bulk semantics in memory.
router.py
EntitySpec (the neutral entity description); CrudHandlers (framework-free async CRUD logic over the seam); build_crud_router(...) (wraps the handlers in a FastAPI APIRouter, importing FastAPI lazily).
examples.py
Two neutral example specs — WIDGET_SPEC (mutable) and EVENT_SPEC (append-only, has_updated_at=False).
__init__.py
Re-exports the public API and __version__.
Module map (frontend, frontend/)
File
Responsibility
src/createCrudStore.js
createCrudStore({ resource }) → a Pinia store definition. Cursor pagination, filters, and optimistic create/patch/delete with rollback. Talks to /<resource> over an injectable http client (default: a small fetch wrapper). Original clean-room code authored from docs/API.md.
tests/createCrudStore.spec.js
Vitest spec driving every flow against a mock http client.
component → useStore() (Pinia store from createCrudStore)
│
├─ fetchFirstPage({filters}) ─► GET /<resource>?limit&filter
│ └─ replace items; remember next_cursor
├─ fetchNextPage() ─► GET /<resource>?cursor=… (append)
├─ create(payload) ─► optimistic insert → POST → swap real row
│ (rollback: remove temp on failure)
├─ patch(id, changes) ─► optimistic update → PATCH → reconcile
│ (rollback: restore prior on failure)
├─ remove(id) ─► optimistic removal → DELETE
│ (rollback: re-insert at original index)
└─ bulk({operation,ids,patch})─► POST /<resource>/bulk → reconcile locally
Seams (external dependencies / injection points)
Seam
Where
Status
Storage backend
CrudStore (passed to CrudHandlers / build_crud_router)
Required injection.InMemoryStore is the dependency-free default; a real adapter wraps a DB driver (e.g. asyncpg/psycopg). The package never imports a DB driver.
Web framework
build_crud_router
Optional, lazy import. FastAPI is imported inside the factory; absent → a clear RuntimeError. The handlers, store, and builders work with no web framework.
Tenant resolution
tenant_resolver (+ auth_dependency)
Required injection for the router. Maps the request's auth object to a tenant_id. The default auth_dependency reads an X-Tenant-Id header (demo-grade); wire a verified-session dependency in production.
Audit emitter
audit_emitter= on CrudHandlers / build_crud_router
Optional injection. Called once per mutation (one per bulk batch). Best-effort: an exception in it never fails the request. Omit it and mutations simply aren't audited.
Id validation
id_validator=
Optional injection. A per-id coercion callable (e.g. uuid.UUID) used by the bulk builder; omit to pass ids through as strings.
Frontend transport
http / baseUrl / headers on createCrudStore
Optional injection. Defaults to a fetch wrapper; pass an http client (e.g. an axios-shaped object, or a test mock) to customize transport / auth.
Design invariants
One contract, two halves. The EntitySpec that shapes the backend names
the routes and the list-response key the frontend consumes. Neither half
imports the other; they agree because they implement the same documented
contract (docs/API.md).
Injection-safe by construction. Filter values are always
driver-parameterized; column names go through a per-entity allow-list; the
table alias is identifier-checked. A new entity cannot opt out of these — it
has no hand-written SQL to opt out in.
Pure policy core.pagination, filters, bulk, and CrudHandlers do
no I/O of their own (the store is the only thing that touches data) and are
unit-testable without a web framework or a database.
Tenant-safe and audited on the path. Every handler is tenant-scoped;
not-found and cross-tenant-hidden are the same 404; every mutation emits one
best-effort audit row.
Degrade open at the seams. A missing audit backend never fails a
mutation. A missing web framework raises only when you ask for a router.
Domain-neutral. No field name, table, constant, or example references any
specific product, industry, or vendor.