CMS / foldkit-cms-drizzle
Example
The following is wiring pseudocode: Blog, Posts, posts, User,
and the insert/update handlers belong to your application (a compiled version
is test/readme.test-d.ts). Use
the runnable server for complete handlers,
a database layer, and transaction setup. sqliteTables() describes tables;
include them in your migrations before running the server.
import { CmsServer, Transaction, sqliteTables } from 'foldkit-cms-drizzle'
import { Guards } from 'foldkit-guard'
import { GuardDrizzle } from 'foldkit-guard/drizzle'
const cmsTables = sqliteTables() // or pgTables(); add them to your schema and migrations
// Who is an author, who may publish, and what a visitor sees: a guard set.
const G = Guards.declare('Cms', User, guard => ({
isAuthor: guard.principal(),
isEditor: guard.principal(),
isPublished: guard.row(Blog.Post, () => Expr.isNotNull(Blog.Post.fields.publishedAt)),
}), { key: user => user.id, unguarded: 'allow' })
const Policy = G.implement({
isAuthor: principal => Option.exists(principal, user => user.role !== 'reader'),
isEditor: principal => Option.exists(principal, user => user.role === 'editor'),
})
// A visitor sees what is published; an author sees every row.
const Post = Blog.Post.pipe(Guards.attach(G, { row: { Read: [G.isPublished, G.isAuthor] } }))
const Db = bind({ ...Blog, Post }, {
Post: { table: posts, visible: GuardDrizzle.visible(Policy, [Post]).Post },
})
// Your own handlers of the two mutations the content type publishes through.
const CreatePost = RemoteServer.mutation(Posts.publish.create, ({ input }) => /* insert */)
const UpdatePost = RemoteServer.mutation(Posts.publish.update, ({ input }) => /* update */)
const cms = CmsServer.make({
tables: cmsTables,
content: [{ type: Posts, entity: Post, binding: Db.Post, create: CreatePost, update: UpdatePost }],
transaction: Transaction.statements, // one connection; Transaction.drizzle for Postgres
policy: Policy,
authors: [G.isAuthor],
// An author writes; only an editor puts it in front of visitors.
transitions: { publish: [G.isEditor], schedule: [G.isEditor], unpublish: [G.isEditor] },
})
RemoteServer.make({
entities: [...cms.sources, source(Db.Author)],
queries: [...cms.queries],
mutations: [...cms.mutations],
})- Register
cms.sources, notsource(Db.Post). They are the content types' sources and the CMS's own, each behind the boundary. - A content type that can be unpublished must be guarded by whether it is.
CmsServer.makethrows for a type with apublishedrole whose Read rule does not read that field, and, throughGuardServer.validate, for a binding whosevisibleis not the rule your guard set built for it.entityis the type's Entity with its guards attached, the one its table was bound from. - A content type with no
publishedrole has nothing to hide, and needs none. - Who is an author is any of
authors, principal guards; anyone else is a visitor. Their bodies are asked with the interaction it is for,Readfor reading unpublished work andUpdatefor any operation on it, so a reviewer can read what they may not change. Who may make a transition is an author any of itstransitionsguards lets; a transition not named is any author's. Archiving an entry on show takes it off, so it asksunpublish's guards too.allowstays for a rule about the entry itself. A refusal isForbidden, with its sentence. createandupdatego to the CMS, not toRemoteServer.make. Registered there too, they are a way to publish with no draft, no revision and noallow.- Content that exists already is imported, not inserted.
cms.import({ type, values, as, at, entry })makes the entry and a draft ofvalues, then publishes it as a publish does: yourcreatehandler writes the row, asas, and revision 1 recordsvalues, datedat. It is one transaction, and the server's own act, soallowis not asked. Writing the CMS's tables by hand has to match what a publish writes, and drifts.
Seeding
A seed is a runner over cms.import — one item per entry, in dependency order:
- Fix the clock. Pass
nowtoCmsServer.makeandatto each import, so every run writes the same dates: the build's pages and every sandbox agree. - Name entries that are pointed at. Pass
entrywhere another import names it (a page naming its featured post), and import what is pointed at first so the id is known. Anything else takes the default new id. - Fix
asonce. Import as an editor and vary the rest; the input isImportItem<P>lessas, so the runner and the tool cannot drift.
The CMS example's seed.ts is the worked shape: dated posts, then the pages
that point at them.
Verify the audience boundary
Before connecting the editor, exercise the same content read as an author and
as a visitor. An author may see working content; a visitor must see only rows
allowed by the binding's visible rule. Repeat through a relation and a query,
not just a direct id read. CMS state and allowed actions are separate from the
identity established by your HTTP or RPC authentication.
For the first write, save a draft and read the public row again: it must be
unchanged. Publishing should then update the row, append a revision, and remove
the draft within the supplied transaction. A scheduled time alone executes
nothing; your host must call cms.due as described below.