// `content.list` — a LIVE, tenant-scoped list of one content type's rows. The // editor subscribes with `useSubscription('cms', 'content.list', { type })`; a // publish/save wakes the subscription so the list updates without a refetch. // `status` picks the table: 'draft' (the editor's working set, the default) or // 'published' (what consumers read). // // The output pins only the columns a list view needs — the runtime drops the // rest (`Schema.Struct` ignores excess), so ONE query shape serves every content // type even though their full rows differ. `title`/`slug` are optional so a type // without them still decodes. import { defineQuery } from '@voltro/protocol' import { Schema } from 'effect' export const listContent = defineQuery({ name: 'content.list', // Editors only. `guards:` is enforced in the dispatch spine — when the // subscription OPENS and again on EVERY delivery — so an editor whose access // is revoked mid-session stops receiving rows instead of keeping a live feed // open on an expired grant. `tenant()` on the derived content tables still // scopes WHICH rows; this decides WHO may ask. // // Satisfiable by this app's own configuration: `voltroPasswordStrategy` // matches the session cookie, `auth.resolveScopes` (authz.ts) grants // `content:read` to the matched subject. Anonymous → no scopes → denied, // which is the point. // // Consumers of PUBLISHED content are a separate surface (`handleCmsRest`, // api-key scoped) — see the README. They do not come through here. guards: [{ scope: 'content:read' }], input: Schema.Struct({ type: Schema.NonEmptyString, status: Schema.optional(Schema.Literal('draft', 'published')), }), output: Schema.Array( Schema.Struct({ id: Schema.String, status: Schema.String, title: Schema.optional(Schema.String), slug: Schema.optional(Schema.String), updatedAt: Schema.Date, }), ), })