QUESTPIE
SchemaSeeds

Checkpointed seeds

seed.steps() trades one transaction for a checkpoint per step, so a seed that fails on its two hundredth upload resumes there instead of starting over.

View markdown

A seed that imports a large fixture, uploads files or calls a paid API is the wrong shape for all-or-nothing. This page turns one into a seed that picks up where it stopped.

Wrapping the work in steps

seed.steps({...}) adds step(name, fn) to the seed context. Everything else about the definition is the same.

src/questpie/server/seeds/demo-content.ts
import { seed } from "questpie/services";

export default seed.steps({
	id: "demoContent",
	description: "Demo posts, resumable",
	category: "dev",
	async run({ step }) {
		const fixture = await step("fetch-fixture", async () => {
			const res = await fetch("https://example.test/fixture.json");
			return (await res.json()) as { title: string; slug: string }[];
		});

		await step("create-posts", async ({ collections }) => {
			for (const post of fixture) {
				await collections.posts.create(post);
			}
		});
	},
});

Run it, kill it halfway, run it again. fetch-fixture does not call the network a second time. It returns the value it returned the first time, and only create-posts executes.

What a step is

Each step() call runs its callback in its own transaction and writes one row to questpie_seed_steps, keyed by seed id and step name. The row holds the callback's return value as JSONB. On the next entry into run, a step whose row is already there skips the callback and hands back the stored value.

The callback receives a seed context bound to that step's transaction, which is why the example destructures collections from it rather than from the outer run. A callback that needs nothing can take no argument at all.

What the return value may be

The type is JSON or nothing: null, booleans, numbers, strings, arrays and plain objects. A step that returns nothing is fine, and on replay it hands back undefined. Name steps deliberately, because the name is the key and a renamed step is a new step.

step is not on a plain seed

step() exists only on the context seed.steps() builds. Reaching for it inside a seed({...}) handler does not compile.

What you give up

A checkpointed seed has no seed-wide transaction. Work outside a step() call is not checkpointed and not rolled back when a later step throws, so keep every database write inside one. The tracking row in questpie_seeds is written after run returns, so a seed that fails midway stays pending with its finished steps recorded.

A failed step runs again in full

The checkpoint is written only once the callback returns. A step that uploads a file and then throws leaves the file behind and no row, so the next run uploads it again. Keep external work in small steps.

Clearing the checkpoints

CommandWhat happens to the step rows
questpie seed --forceCleared with the tracking row. Every step reruns.
questpie seed:undoThe undo handler runs, then the rows go.
questpie seed:resetRows go. No handler runs and no data changes.
questpie seed --validateWritten inside the dry run, thrown away with it.

A forced run that fails partway leaves the seed pending with whatever steps it did finish, so the next run resumes from there rather than from the top.

  • Seeds for the plain form and its single transaction.
  • Running seeds for the flags in that table.
  • Jobs for work that belongs on a queue instead of in a seed.

On this page