@tailor-platform/sdk-plugin-seed
v0.3.6
Published
Tailor CLI plugin providing the `tailor seed` commands (TailorDB and IdP seed data)
Readme
@tailor-platform/sdk-plugin-seed
Tailor CLI plugin that provides the tailor seed commands: seed TailorDB (and IdP _User) data from JSONL files generated by seedPlugin, dump the rows currently in TailorDB back out as seed data, validate that data against the generated schemas, and fill in the create-time values the data needs, such as the ids that express relations.
[!NOTE] This package is a CLI plugin: it ships an external
tailor-seedexecutable that the Tailor CLI dispatches to when you runtailor seed. KeepseedPluginfrom@tailor-platform/sdk/plugin/seedindefinePlugins()— it generates the seed data and schema files this plugin consumes.
Installation
Install it next to @tailor-platform/sdk in your project:
npm install -D @tailor-platform/sdk-plugin-seed@nextThe Tailor CLI discovers the plugin automatically from node_modules/.bin (or your PATH). Run tailor plugin list to confirm it resolves.
Usage
# Seed everything (TailorDB tables, then IdP _User)
tailor seed apply
# Truncate target tables first, without a confirmation prompt
tailor seed apply --truncate --yes
# Seed a single TailorDB namespace (excludes _User)
tailor seed apply --namespace my-db
# Seed specific tables only
tailor seed apply User Order
# Validate JSONL seed data against the generated schemas
tailor seed validate
tailor seed validate ./seed/data/User.jsonl
# Fill in the ids rows are missing (and any other create-time field)
tailor seed fill
tailor seed fill --fields id,createdAt
tailor seed fill ./seed/data/User.jsonl
# Fill, then check the result
tailor seed fill && tailor seed validate
# Update existing rows (matched by id, or by name for _User) instead of failing on duplicates
tailor seed apply --upsert
# Write the rows currently in TailorDB out as JSONL seed data
tailor seed dump --out ./snapshot
tailor seed dump --namespace my-db --out ./snapshot
tailor seed dump --force User OrderValidating seed data
tailor seed validate checks every row against the table it seeds: field types, required fields, the table's own validate, and the relations between files. A row is also rejected when it carries a field the table does not declare, at the top level or inside a nested object:
seed/data/Company.jsonl:3 • legacyCode: Field "legacyCode" is not declared by the table. Remove it from the row, or add it to the table definition and run `tailor generate`.This is what catches seed data that has drifted from the table definition — a column that still exists in a deployed environment but was removed from tailor.config.ts, or a typo in a field name — before tailor seed apply turns it into a database error. The check follows the generated schema files, so run tailor generate after changing a table or attaching a plugin to one. Fields a plugin adds to the table are validated like the table's own. IdP _User rows are not checked this way, since their extra keys are user attributes.
Dumping the current data back out
tailor seed dump reads the rows a deployed app currently holds and writes them as JSONL, one file per table, in the format tailor seed apply reads. That makes a dump the restore point for the change you are about to make:
tailor seed dump --force
# ... run the migration, the executor, the workflow you want to try ...
tailor seed apply --truncate --yes --skip-idpNaming tables limits the dump to them, and --namespace limits it to one namespace, so a table you are about to change can be captured and put back on its own rather than resetting the whole app.
Restoring needs --skip-idp unless you also have a separate backup of _User. A dump never captures _User (see below), so tailor seed apply --truncate without --skip-idp would truncate the app's current IdP users and reseed _User from whatever _User.jsonl already has on disk — data the dump did not just refresh, and that may no longer match who has actually signed up. Add --skip-idp to the restore unless you have separately backed up and intend to restore _User too.
Fields the platform assigns rather than the seed row — serial fields — are left out, unless another table's relation is keyed to that field, in which case it is kept so the relation still resolves after apply --truncate gives the row a fresh value. Explicit nulls are kept as-is, so a dumped line reads back the way a hand-written one does and tailor seed validate accepts it. Rows are read a page at a time, ordered by id, and written to disk as each page arrives rather than held in memory for the whole table; --page-size changes how many come back per request.
Without --out, the files land in the seed data directory under the seedPlugin distPath, on top of the data that is already there — so a file that already exists is only overwritten with --force:
2 JSONL file(s) already exist in /project/seed/data: User, Order. Pass --force to overwrite them, or --out to write elsewhere.IdP _User records are never dumped. Their credentials do not survive a round trip through seed JSONL, so naming _User is an error rather than a partial export; keep that data under version control instead.
Two things the dump does not capture: the bytes behind stored file fields, and which rows came from a previous seed rather than from the app — a dump is the whole current state of the tables it covers.
Paging is not safe against concurrent writes. Each page is fetched with id as a keyset cursor, but TailorDB ids are UUIDs rather than a monotonic sequence, so a row inserted after paging starts can land before the cursor already handed out and never be seen. Dumping a table that a running app keeps writing to can therefore miss rows written during the dump; for a true point-in-time restore point, pause writes (or dump from a replica/snapshot) before running tailor seed dump.
The machine user needs unconditional read access. TailorDB read permission conditions filter rows per record, so a machine user that can only see a subset of a table still gets a successful result back — just a smaller one — and the dump reports that subset as the complete table. Restoring from it with apply --truncate then permanently drops the rows the machine user could not see, with no error at any point. Grant the machine user used for tailor seed dump unconditional read access to every table you dump.
Filling in create-time values
A relation in seed data points at a field of the row it references — usually its id — so a row you want to reference needs an id written in the file. tailor seed fill writes the value a record would get on create into the rows that are missing it, id by default:
# ./seed/data/Customer.jsonl before
{"name":"Acme Corporation","email":"[email protected]"}
# after
{"id":"0b6b6f5e-3b8a-4d0e-9a1f-2c7d8e5a4b31","name":"Acme Corporation","email":"[email protected]"}--fields names what to fill, so any field the table gives a value to on create can be written the same way — --fields id,createdAt also stamps a creation time on rows that have none, which is how you seed records that need to look older than the seed run:
tailor seed fill --fields id,createdAtThe values come from the table itself — its id, its field defaults, its create hooks — applied to each row on its own.
Nothing is validated. That is deliberate: the ids are what you need in order to write the rows that reference them, so waiting for the data to be valid would be waiting for the thing this command gives you. A row can be filled while a required field is still missing, or while another file references an id that does not exist yet. Run tailor seed validate once the data is ready.
Only the named fields are written, and only into a row that has no value for them, so a value already in the file is never replaced — a row that already carries a createdAt keeps it. A field the table gives no value to is skipped: --fields id leaves the IdP _User data alone, whose rows are identified by name, and a field the platform assigns rather than the table — a serial field — has nothing to fill in from here:
⚠ No seed data produces a value for: invoiceNumber
✓ Nothing to fillA line that gains nothing is left exactly as it was, byte for byte. Only the lines that take a value are rewritten, and those get their keys in the order the table declares its fields, so a filled-in id lands at the front of the line and a createdAt next to the other timestamps. Keys the table does not declare follow the declared ones.
The values are read from the schema files seedPlugin generates next to the data. Every one of them is read before anything is written, so a project that has not run tailor generate since upgrading is told which file to regenerate and keeps its data untouched:
./seed/data/Customer.schema.ts does not export `hook`. Run `tailor generate` to regenerate the seed schema files.Naming a single .jsonl file limits the run to that file, so a referenced table can be filled on its own before the rows that reference it are written.
Without --upsert, a row whose id already exists in a target table fails the seed run. With --upsert, every row must supply an id (and any field the table requires) — run tailor seed fill first if some rows have none — and a matching row is updated in place instead. Because the update goes through the same write path as any other update, it runs update hooks and validation and updates fields such as updatedAt, and it publishes a record-updated event — so an executor using recordUpdatedTrigger fires for each existing row that gets updated.
The machine user used for seeding comes from --machine-user or the machineUserName seedPlugin option:
definePlugins(
seedPlugin({
distPath: "./seed",
machineUserName: "admin-machine-user",
}),
);Run tailor seed <command> --help for the full option reference.
