6.2.1 Schema + service + API — `saved_filter` (versioned AST envelope, visibility, stars) + permission matrix + built-in defaults + the 6.3 data-source contract
Estimate: 34m · Depends on: 6.1.1, 6.4
The persistence + permission layer. Pure backend — no UI.
Schema (one migration, FKs as Prisma @relations): saved_filter — project FK, owner FK, name (unique per project, case-insensitive — cite the story decision), description?, visibility enum { private, project }, and the versioned AST envelope (JSONB carrying exactly what the 6.1.1 codec encodes, version field included); saved_filter_star — (filter, user) unique pair. Timestamps throughout.
Service + repos (4-layer): CRUD with the permission matrix — create/star: any project member incl. viewer (private only for viewers); visibility project requires role ≥ member; update/delete/change-owner: owner OR project admin (workspace owner/admin always); every read filtered by the 6.4 browse gate + visibility. Resolve (THE data-source contract, documented for 6.3 in the service JSDoc): id → decode + registry-validate the stored envelope via 6.1.1 (NEVER trust-and-compile) → the compiled WHERE fragment + metadata DTO; stale referents degrade per the 6.1.2 unknown-value rule; malformed/future-versioned envelopes → the typed recoverable error. List reads bounded + server-searched + paginated (mine / project / starred views; star counts aggregated in SQL — finding #57). Built-in defaults: the system filters as non-persisted AST constants (My open issues, Reported by me, All/Open/Done issues, Created/Updated/Resolved recently) exposed through the same list/resolve reads with builtin: true (no write paths). Delete dependents: the delete read enumerates dependents (subscriptions now; 6.3 widgets join in by FK later) so the UI can render the Cloud-style warning; deletion cascades subscriptions in the same transaction.
Acceptance criteria
- Migration applies cleanly (re-run: no drift); both FKs modelled as
@relations; the name-uniqueness constraint is case-insensitive per project. - The permission matrix holds for every (role × visibility × action) cell — matrix-tested, incl. the viewer private-only rule and admin powers over others' shared filters; private filters never appear in another user's reads (asserted at the service AND route layer).
- Persist→resolve round-trips every constructible AST (property test reusing the 6.1.1 generators); stale-referent and malformed-envelope paths yield the typed degraded states; built-in defaults resolve and reject writes.
- List reads are paginated + server-searched; an EXPLAIN spot-check shows no full-table star aggregation;
pnpm test:coverage≥90%.
Context refs
- 6.1.1
lib/filters/ast.ts(the envelope + codec — the single source of the stored shape) + the 6.1.2 stale-referent rule - Story 6.4 roles/gates (
ProjectMembership, access levels) — the permission substrate;motir-core/CLAUDE.md(4-layer, required-tx, FK@relationrule) - The verified Jira facts in the Story 6.2 description (Save/Save-as ownership, admin powers, system-filter immutability)
- finding #57 (bounded reads)