Skip to content

Latest commit

 

History

History
97 lines (84 loc) · 6.96 KB

File metadata and controls

97 lines (84 loc) · 6.96 KB

Architecture

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.
package.json / vitest.config.js Vue 3 + Pinia peers; vitest runner.

Data flow (backend)

HTTP request  →  FastAPI route (build_crud_router)
      │                 │
      │                 ├─ auth_dependency  →  tenant_resolver  →  tenant_id
      ▼                 ▼
CrudHandlers.<op>(tenant_id, ...)            (framework-free; unit-testable)
      │
      ├─ list   ─► FilterSpec.from_dict ─► (store enforces allow-list)
      │           store.list_rows ─► decode_cursor / encode_cursor (keyset)
      ├─ get    ─► store.get_row ─► NotFound → 404
      ├─ create ─► validate columns ─► store.create_row ─► audit("create", diff)
      ├─ patch  ─► allow-list keys ─► store.patch_row ─► audit("update", before/after)
      ├─ delete ─► store.delete_row ─► audit("delete")
      └─ bulk   ─► build_bulk_statement ─► store.bulk ─► audit(bulk_*, count) [ONE row]
      │
      ▼
CrudStore seam  (InMemoryStore by default; a Postgres adapter in production)

Data flow (frontend)

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.