// `content.saveDraft` — upsert a draft. The executor runs the CMS pipeline // (derive `slug` from `title`, validate every field rule, write to // `_drafts`). Validation failures don't crash: they come back as a typed // RESULT union (`{ ok: false, violations }`) the editor annotates the form from, // mirroring `contentViolations` on the client. Omit `values.id` to create; pass // it to update your own draft in place. // // The descriptor stays pure effect/Schema (no @voltro/cms import) so it is // browser-safe with nothing extra to pull into the bundle — the result union is // what carries the validation outcome across the wire. import { defineMutation } from '@voltro/protocol' import { Schema } from 'effect' const Violation = Schema.Struct({ field: Schema.String, rule: Schema.String, message: Schema.String, }) export const saveDraft = defineMutation({ name: 'content.saveDraft', // The write surface the README promises is editors-only — enforced here // rather than described. `guards:` runs in the dispatch spine BEFORE the // executor and before the transaction opens, so an unauthorized save never // touches the database. // // Satisfiable by this app as shipped: sign in via POST /auth/sign-in, // `voltroPasswordStrategy` matches the cookie, `auth.resolveScopes` // (authz.ts) grants `content:write`. No cookie → anonymous → no scopes → // `ScopeError`, which is exactly the boundary this template exists to show. guards: [{ scope: 'content:write' }], input: Schema.Struct({ type: Schema.NonEmptyString, // Arbitrary per-type field values (may carry `id` to update an existing // draft). Validated on the server against the content type's rules. values: Schema.Record({ key: Schema.String, value: Schema.Unknown }), }), output: Schema.Union( Schema.Struct({ ok: Schema.Literal(true), id: Schema.String, status: Schema.String }), Schema.Struct({ ok: Schema.Literal(false), violations: Schema.Array(Violation) }), ), })