QUESTPIE
SchemaGlobals

Versions and stages

Turning on versioning snapshots every write of a global into a history table, and nesting a workflow under it adds draft and published stages you move between.

View markdown

Someone edits the footer at four on a Friday and the wrong link goes live. You want the previous copy back, and next time you want the change to sit in draft until an editor releases it. Both come from one option.

Keeping a history

src/questpie/server/globals/site-settings.ts
import { global } from "#questpie/factories";

export const siteSettings = global("site_settings")
	.fields(({ f }) => ({ siteName: f.text().required().default("My Site") }))
	.options({ versioning: true });

Every write now snapshots the row into site_settings_versions, numbered from one. Two methods work on that history:

const history = await app.globals.site_settings.findVersions({ limit: 10 });
//    oldest first, each entry carries the field values of that snapshot
//    plus versionId, versionNumber, versionOperation, versionUserId,
//    versionCreatedAt and sourceRevision

await app.globals.site_settings.revertToVersion({ version: 3 });

revertToVersion() also takes { versionId }. It needs one of the two and throws 400 without either. findVersions() enforces the same read rule as get(), and returns an empty array when versioning is off.

The server type for findVersions() declares the metadata only, so TypeScript will not show you the field values the query does return. The client's type carries them.

History is capped

maxVersions defaults to 50. Older snapshots are deleted as new ones land. Raise it with versioning: { maxVersions: 200 }.

Draft and published

Nest a workflow under versioning. Stages are stored as version snapshots, so they cannot exist without it.

.options({ versioning: { workflow: true } })   // stages: draft, published

Writes land on the initial stage, draft. Move the global forward on its own, without touching the data:

await app.globals.site_settings.transitionStage({ stage: "published" });

const live = await app.globals.site_settings.get({ stage: "published" });

Reading a stage other than the initial one reads the newest snapshot carrying that stage, so get({ stage: "published" }) returns null until a snapshot lands there. Reading without a stage returns the live row.

Custom stages

.options({
	versioning: {
		workflow: {
			stages: ["draft", "review", "published"],
			initialStage: "draft",
		},
	},
})

stages also takes a keyed object, { draft: { transitions: ["review"] }, … }, where transitions lists the stages you may move to next. Leave transitions off and any stage can follow any other. An initialStage or a transition target outside stages throws the first time the global is used, not at boot. Asking for an unknown stage at runtime, or making a move transitions forbids, throws 400.

What a workflow adds

ThingEffect
transitionStage()Moves stage and writes a snapshot, no data change
beforeTransition afterTransitionBoth see fromStage and toStage. Only the first aborts on throw, the second is logged
access.transitionIts own rule, falling back to access.update
stage on get() and update()Which snapshot to read, which stage to write to

Pass scheduledAt with a future date and the transition is queued instead of run, after the target stage, the record, the access rule and the transition graph have all been checked. This needs a queue adapter and throws without one.

Optimistic concurrency

optimisticConcurrency: true adds a framework-owned revision column and makes every write name the revision it expects.

.options({ versioning: true, optimisticConcurrency: true })
await app.globals.site_settings.update({
	data: { siteName: "QUESTPIE" },
	expectedRevision: 1,
});

The patch moves inside a data key, and expectedRevision sits beside it. revertToVersion() and transitionStage() take it too. A mismatch throws 409. An absent row counts as revision 0, and the row starts at 1 once it exists. You cannot declare a field called revision, the constructor throws.

Full behaviour is on Optimistic concurrency.

Switching versioning off breaks a workflow

versioning: { enabled: false, workflow: true } fails when the app builds the global. Stages are version snapshots, so switching versioning off removes the thing they are stored in.

The tables

<name>_versions holds versionId as its primary key, the source row's id, versionNumber, sourceRevision, versionOperation, versionStage, versionFromStage, versionUserId, versionCreatedAt, and a copy of every unlocalized field plus the source row's timestamps. It is indexed on (id, version_number), (id, version_stage, version_number) and version_created_at.

Localized fields go to <name>_i18n_versions, one row per locale per version, unique on (parent_id, version_number, locale).

On this page