MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-335

6.2.1 Schema + service + API — `saved_filter` (versioned AST envelope, visibility, stars) + permission matrix + built-in defaults + the 6.3 data-source contract

Done
Description

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 @relation rule)
  • The verified Jira facts in the Story 6.2 description (Save/Save-as ownership, admin powers, system-filter immutability)
  • finding #57 (bounded reads)
Status
Done
Type
Sub-task