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.
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.
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
| Command | What happens to the step rows |
|---|---|
questpie seed --force | Cleared with the tracking row. Every step reruns. |
questpie seed:undo | The undo handler runs, then the rows go. |
questpie seed:reset | Rows go. No handler runs and no data changes. |
questpie seed --validate | Written 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.
Related
- 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.
Purge
purgeById physically removes a row that is already soft-deleted. It has its own access rule, its own hooks, and it fails closed on anything still pointing at the row.
Running seeds
The options each seed command takes, the dry run that rolls itself back, the config key that seeds on startup, and the same four operations from code.