feat(seed): add tailor seed fill to fill in create-time values for seed data - #1975
Merged
Conversation
…seed data A relation in seed data points at a field of the row it references, usually its id, so a row that others reference needs an id written in its JSONL line. The new command writes the value a record gets on create into the rows that are missing it: `id` by default, and `--fields` names any other create-time field, so a row can also be stamped with a `createdAt` that predates the seed run. `apply --upsert` names the command when it rejects a row for having no id, which is the run that concretely blocks on one. Only the named fields are written back, which @toiroakr/lines-db 0.11.0 made possible: fields the caller did not name are no longer materialized into the file, optional fields a line omits stay omitted, and lines keep the order the file lists them in. A field a type does not have is skipped for that type, so one field list covers a whole data directory and `--fields id` leaves the IdP _User data alone. A file missing none of the fields is not rewritten at all. A file that does get rewritten has its keys ordered the way the type declares its fields, since a write-back appends a key the line did not have and would otherwise leave a filled-in id at the end of the line, where no hand-written seed row puts it. Keys the type does not declare follow the declared ones. Whether an existing value survives is the type's own create behaviour: `id`, a field default, and a create hook that returns its input only fill the gaps, while a hook that ignores its input recomputes the field. The data is validated first and nothing is written when validation fails. The same operation is available as `fillSeedData` from `@tailor-platform/sdk/seed`.
🦋 Changeset detectedLatest commit: 9da2117 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@tailor-platform/create-sdk
@tailor-platform/eslint-plugin-sdk
@tailor-platform/sdk
@tailor-platform/sdk-plugin-seed
@tailor-platform/sdk-plugin-tailordb-erd
commit: |
This comment has been minimized.
This comment has been minimized.
… regex zod is already a dependency of the package, and `z.uuid()` is what the CLI itself validates ids with, so the test no longer carries its own copy of the UUID shape. The assertion message keeps the offending value visible, which a bare `safeParse().success` boolean would have dropped.
This comment has been minimized.
This comment has been minimized.
Two ways the fill could damage a file it was asked to complete. Key ordering tested membership with `key in row`, which walks the prototype chain, so a row carrying a key named `toString` or `constructor` read as already-placed and was dropped from the line it was written back to. Ordering now goes through `Object.entries` / `Object.fromEntries` and a rank lookup, which stay on own properties, so such a key survives and a `__proto__` key never reaches an inherited setter. Naming a field the platform assigns rather than the type — a serial field — wrote `"invoiceNumber":null` into every row, because the field is declared but left unset for seeding, and a null was still a value to write back. A field now counts as fillable only where the type gives every loaded row a value for it, so a serial field falls into the existing "no seed data produces a value for" warning and nothing is written.
The ordering ran through a plain object, so a row carrying `__proto__` hit an
inherited setter and lost the key just as `toString` did. Both are asserted on
one row now: against the previous implementation the written line came back as
`{"id":"x","name":"first"}` with both keys gone.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
A generated project only learned about the fill command by reading the plugin README. The script sits next to the read-only `seed:validate`, which stays as it is so CI keeps a seed check that writes nothing. It runs `tailor seed fill` alone rather than `fill && validate`: fill already validates before it writes and exits non-zero on invalid data, and it only writes values that validation just accepted, so a following validate would repeat the expensive half of the run for no added coverage.
This comment has been minimized.
This comment has been minimized.
The JSDoc, the command notes, and the README all read "every other value each
line already held stays exactly as it was", which is true of the values but
invites reading the line itself as untouched. A file that gets rewritten goes
back out through JSON serialization, so a line that was not already in that
form comes back reformatted even when the fill did not name any of its fields:
before { "id":"60000000-…", "amount":1.50e5, "status":"paid" }
after {"id":"60000000-…","amount":150000,"status":"paid"}
That row was not a fill target — it already had an id — and 1.50e5 and 150000
are the same value, but the bytes differ. The three places now say the values
are kept and that a rewritten file has its formatting normalized. A file that
is missing none of the named fields is still not rewritten at all.
The command notes also still described the skipped fields as ones "a type does
not have", which the fill now decides by whether the type gives every row a
value for them.
This comment has been minimized.
This comment has been minimized.
…atform `fillSeedData` reports the path of each file it wrote, both in the printed line and in `--json`. Built with `node:path`, that path carried whatever separator the platform uses, so the same run described the same file two ways: C:\Users\me\AppData\Local\Temp\seed\Widget.jsonl (node:path on Windows) C:/Users/me/AppData/Local/Temp/seed/Widget.jsonl (pathe, everywhere) `pathe` is what the rest of the package reaches for, and this module was one of the few places still on `node:path`. Switching it also settles the fill tests, which build the path they expect with `pathe` and would not have matched the reported one on Windows — the seed plugin suite runs on Linux today, so nothing was failing yet.
This comment has been minimized.
This comment has been minimized.
On a type declaring a field named `__proto__`, `row[field] = value` goes through the inherited setter, so the serializer finds no own property and the line gains nothing the run said it filled. `createStandardSchema`'s docstring also read as if both levels of validation always report together, when the field schema runs only after the type's own `validate` reported nothing.
This comment has been minimized.
This comment has been minimized.
A line that is not a JSON object was skipped without a word, so a file whose only fillable row has a typo in it reported `Nothing to fill` and left the reader looking for a field that was never the problem.
This comment has been minimized.
This comment has been minimized.
They come from a separate config that `pnpm generate` does not cover, so they still held the non-exported hook and the two-argument `createStandardSchema` the generator no longer emits.
`readdir` order follows the filesystem, and the fill reports what it did per table, so the same data directory read differently on another machine.
toiroakr
force-pushed
the
feat/seed-fill-ids
branch
from
August 5, 2026 13:16
dfb9676 to
9da2117
Compare
Code Metrics Report (packages/sdk)
Details | | main (542c5e9) | #1975 (635524c) | +/- |
|--------------------|----------------|-----------------|-------|
+ | Coverage | 78.5% | 78.5% | +0.0% |
| Files | 462 | 462 | 0 |
| Lines | 17731 | 17738 | +7 |
+ | Covered | 13920 | 13927 | +7 |
- | Code to Test Ratio | 1:0.4 | 1:0.4 | -0.1 |
| Code | 126620 | 126830 | +210 |
+ | Test | 61927 | 61935 | +8 |Code coverage of files in pull request scope (61.9% → 64.2%)
SDK Configure Bundle Size
Runtime Performance
Type Performance (instantiations)
Reported by octocov |
remiposo
approved these changes
Aug 7, 2026
Contributor
There was a problem hiding this comment.
[nits]
This applies to other existing documentation as well, but it's a bit concerning that the docs still use type even though definitions are now done with db.table().
Contributor
Author
There was a problem hiding this comment.
I will fix it separately.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
tailor seed fill [path], which fills in the values a record gets on create for the JSONL seed rows that are missing them. A relation in seed data points at a field of the row it references, usually its id, so a row that others reference needs an id written in its line — and until now there was no way to get one there other than typing UUIDs by hand.--fields(defaultid) names what to fill, so any field the type gives a value to on create can be written the same way.--fields id,createdAtalso 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 apply --upsertnow names the command when it rejects a row for having no id, which is the run that concretely blocks on one.seed:fillscript next toseed:validate, so the command is discoverable without reading the plugin README.fillSeedDatafrom@tailor-platform/sdk/seed, next to the existingvalidateSeedData, whose path resolution and error formatting it now shares.validatemoved out ofcreateTailorDBHookand intocreateStandardSchema, so computing a record's create-time values and deciding whether a record is acceptable are separate again.Notes
Writing back only the named fields is what
@toiroakr/lines-db0.11.0 (#1964) made possible. Before it, a write-back materialized every column, so a fill would have baked hook-computed values and omitted optional fields into the file.id, its field defaults, its create hooks — applied to each row on its own, and nothing is validated. That is the point: the ids are what you need in order to write the rows that reference them, so a row can be filled while a required field is still blank or while another file references an id that does not exist yet.validatestays the command that says whether the data is ready, which is whyseed:fillandseed:validateare separate scripts.idat the end of the line, where no hand-written seed row puts it. Keys the type does not declare follow the declared ones.tailor generate; until then the fill names the file to regenerate. Every hook is loaded, and every line decided, before any file is written, so a stale schema file or a throwing hook leaves nothing half-filled.--fields idleaves the IdP_Userdata alone, whose rows are identified byname, and naming a field the platform assigns instead of the type — aserialfield — writes nothing and reportsNo seed data produces a value for: .... A field no seeded type produces is a warning rather than an error, so a data directory holding only_User, and an idempotent re-run, do not fail.Where type-level validation lives
createTailorDBHookfrom@tailor-platform/sdk/testused to run the type's ownvalidateand throw on the first offending record, which is what the fill had to opt out of. Rather than give the hook a flag, the validation moved to where the field-level validation already was:createStandardSchematakes the type as a third argument and reports type-level issues through its result.That also fixes how
tailor seed validatereports them. A failure used to end the run at the first offending row with a bare message; it now lands in the same per-row report as every other issue, naming the file and every row that fails. The header no longer carries two markers either, now that the command hands the CLI a report that is already formatted.A test calling
createTailorDBHookdirectly to assert a type-levelvalidatethrows needs to go throughcreateStandardSchemainstead.