// Deleting a note needs BOTH forms of authorization, which is why this file // exists next to `notes.create`. // // • The declarative guard below is the coarse gate: you need `notes:delete` // to be in this conversation at all. It is checked before the transaction. // • The executor then makes a decision the descriptor CANNOT: a note that is // already archived may be hard-deleted, an active one is only soft- // deleted unless the caller also holds `notes:purge`. That depends on the // loaded ROW, which no descriptor guard can see. // // The rule of thumb the two illustrate: put in `guards:` everything decidable // from the SUBJECT and the INPUT; use `permission()` / `can()` only for what // needs loaded data. Guards are statically checkable, in-handler calls are not. import { defineMutation } from '@voltro/protocol' import { Schema } from 'effect' /** * Lives HERE, in the descriptor file, because the web client imports this * module value-level and must be able to construct/match the error. Keeping * typed errors in a file with zero server imports is what stops a descriptor * from dragging the database handle into the browser bundle. */ export class NoteNotFound extends Schema.TaggedError()('NoteNotFound', { id: Schema.String, }) {} export const deleteNote = defineMutation({ name: 'notes.delete', target: { table: 'notes', op: 'update' }, guards: [{ scope: 'notes:delete' }], input: Schema.Struct({ id: Schema.NonEmptyString, }), output: Schema.Struct({ id: Schema.String, // 'hard' when the row was removed, 'soft' when it was only archived. mode: Schema.Literal('hard', 'soft'), }), // `ScopeError` is merged in automatically by `guards:` — only the app's own // error needs declaring. error: NoteNotFound, })