Seeding
On startup the server can load sample data from a Cypher file. This page describes the file format, when the server loads it, and how to replace it with your own data.
The seed file
seed/movies.cypher (path set by SEED_PATH) is a plain Cypher script:
- Statements end with a semicolon at the end of a line. The server splits the file on
;followed by a newline and runs each statement in order. A semicolon inside a line (for example inside a string) does not split. - Lines starting with
//are comments. A chunk that is only comments is skipped. - The first lines should be the constraints, then the data, then any derived values:
// seed-version: 3
CREATE CONSTRAINT movie_title IF NOT EXISTS FOR (m:Movie) REQUIRE m.title IS UNIQUE;
UNWIND [ {title:"The Matrix", released:1999, genres:["Sci-Fi"]} ] AS f
MERGE (m:Movie {title: f.title}) SET m.released = f.released
FOREACH (g IN f.genres | MERGE (ge:Genre {name: g}) MERGE (m)-[:IN_GENRE]->(ge));
- Use
MERGE, notCREATE, for data. Reloading is then safe and idempotent. - When you write maps in Cypher, keys are bare words (
title:), not quoted strings. - The bundled file was generated by a one-off script that is not in the repo: it is a batch of
UNWINDstatements (20 films each), a fewSETstatements for taglines and birth years, and the derived-counts statement. Edit it directly; to change a few records addMATCH ... SET ...;lines before the last statement. - Unique constraints (
REQUIRE ... IS UNIQUE) are what make titles and names usable as keys. They are also why creating a duplicate returnsConstraint validation failed.
When the server loads it
The decision is made by decideSeed in src/seed.ts using three facts: how many nodes with the probe label exist (SEED_PROBE_LABEL, default Movie), the version stored in a marker node (:SeedMeta {name: 'movies'}), and the // seed-version: N header in the file (1 if absent).
Figure: what the server does with the seed file on start. Click the image for full size; the numbered notes below carry the same information as text.
- Seeding on. Everything below happens only when
SEEDis notfalse. The seed file’s// seed-version: Nheader gives its version (1 if missing). - Two facts from Neo4j. Is there a
(:SeedMeta {name: 'movies'})marker and with what version, and how many nodes carry the probe label (SEED_PROBE_LABEL, defaultMovie). - Reload by bumping the version. If the marker exists and the file’s version is newer, the seed loads again (it uses
MERGE, so this is safe) and the marker is updated. Same or older: skip. - Foreign data is protected. No marker but nodes with the probe label exist: they are someone else’s data, so nothing is loaded. An empty database loads and gets a marker.
| Database state | Result |
|---|---|
| No marker, no nodes with the probe label | Load, then write the marker with the file’s version. |
| Marker exists, file version is newer | Load again (safe because of MERGE), then update the marker. |
| Marker exists, file version is the same or older | Skip (“already loaded”). |
| No marker, but nodes with the probe label exist | Skip: it is someone else’s data, so it is left alone. |
Consequences:
- Pointing the server at your own Neo4j with its own
Moviedata never overwrites it. SetSEED=falseto be explicit. - To ship an updated seed, bump
// seed-version. Existing databases reload it on the next start. - A database seeded before versioning existed (4 movies, no marker) is treated as foreign and is not upgraded. Reset it with
docker compose down -v. - Changing the file without bumping the version does nothing on a database that already has the marker.
Derived counts
Person.actedCount and Person.directedCount are stored on the nodes so people can be sorted and filtered by film count in plain GraphQL (computed @cypher fields cannot be sorted). The last statement of the seed sets them:
MATCH (p:Person) SET p.actedCount = COUNT { (p)-[:ACTED_IN]->() }, p.directedCount = COUNT { (p)-[:DIRECTED]->() };
After startup the server recalculates them after every mutation, so they stay correct when data is edited through the API (mutations). If you change relationships with Cypher directly, run the statement above yourself.
Using your own data
- Write your constraints and
MERGEstatements in a Cypher file and set// seed-version: 1at the top. - Set
SEED_PATHto the file andSEED_PROBE_LABELto the label of your main entity (for exampleBook), so the foreign-data check looks at the right nodes. - If you have no derived counts, set
REFRESH_COUNTS=falseso the plugin does not run the Person count statement. - Start with an empty database. The
grandstackumbrella repo has a worked example of all of this end to end indocs/build-your-own-app.md.
Sample-data caveat
Titles, casts and relationships are real, but ratings and runtimes are approximate and a few casts may be incomplete. Treat the data as illustrative.
Source: grandstack-server/docs/seeding.md (this page is a copy; edit it in the repository)