// The authz that needs the ROW. `notes:delete` was already enforced by the // descriptor guard; what is decided here could not have been: // // archived note → hard delete // active note + notes:purge → hard delete // active note, no purge → archive instead (soft) // // `can()` is the non-throwing form — it BRANCHES rather than refusing, which // is the right shape when the caller is authorized to do *something* and the // scope only selects which. Use `permission()` (Effect, typed failure) when // the answer is "you may not do this at all". // // Note that `notes:purge` is granted by NO named role in app.config.ts — only // the wildcard `admin` role reaches it. That is deliberate and worth // understanding: scopes used in `can()` / `permission()` are invisible to // `voltro check`, so they are NOT validated against the declared vocabulary. // A typo here fails silently as a branch that is never taken; a typo in a // descriptor `guards:` is caught before the app boots. One more reason to // prefer the declarative form wherever the decision doesn't need loaded data. import { EffectStore } from '@voltro/runtime' import type { AppContext } from '@voltro/runtime' import { can } from '@voltro/plugin-rbac' import { eq } from '@voltro/database' import { Effect } from 'effect' import { database } from '../database/schema' import { NoteNotFound } from './notes.delete.mutation' const execute = (input: { id: string }, ctx: AppContext) => Effect.gen(function* () { const store = yield* EffectStore // tenant() scopes this read to the caller's tenant automatically, so a // cross-tenant id simply does not resolve. // `.where(...)` takes the predicate AST (`eq(...)`), not three arguments. const rows = yield* Effect.promise(() => ctx.store.query(database.notes.where(eq('id', input.id)).limit(1).descriptor), ) const note = rows[0] if (note === undefined) { return yield* Effect.fail(new NoteNotFound({ id: input.id })) } const hard = note['archived'] === true || can(ctx, 'notes:purge') if (hard) { yield* store.delete('notes', input.id) return { id: input.id, mode: 'hard' as const } } yield* store.update('notes', input.id, { archived: true }) return { id: input.id, mode: 'soft' as const } }) export default execute