Skip to content

feat(seed): add tailor seed fill to fill in create-time values for seed data - #1975

Merged
toiroakr merged 21 commits into
mainfrom
feat/seed-fill-ids
Aug 7, 2026
Merged

feat(seed): add tailor seed fill to fill in create-time values for seed data#1975
toiroakr merged 21 commits into
mainfrom
feat/seed-fill-ids

Conversation

@toiroakr

@toiroakr toiroakr commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add 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 (default id) names what to fill, so any field the type 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 apply --upsert now names the command when it rejects a row for having no id, which is the run that concretely blocks on one.
  • A generated project gets a seed:fill script next to seed:validate, so the command is discoverable without reading the plugin README.
  • The same operation is exported as fillSeedData from @tailor-platform/sdk/seed, next to the existing validateSeedData, whose path resolution and error formatting it now shares.
  • Type-level validate moved out of createTailorDBHook and into createStandardSchema, 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-db 0.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.

  • The values come from the type itself — its 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. validate stays the command that says whether the data is ready, which is why seed:fill and seed:validate are separate scripts.
  • 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 — not even by a create hook that would recompute it. A run over a data directory therefore cannot invalidate the ids existing rows already reference.
  • A line that gains nothing is written back byte for byte; only the lines that take a value are re-serialized. Those get their keys in the order the type declares its fields, because a write-back appends a key the line did not have, which 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.
  • The generated seed schema files now export the type's create hook, which is where the values come from. Existing projects need 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.
  • Only a field the type gives every loaded row a value for is fillable, so one field list covers a whole data directory: --fields id leaves the IdP _User data alone, whose rows are identified by name, and naming a field the platform assigns instead of the type — a serial field — writes nothing and reports No 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

createTailorDBHook from @tailor-platform/sdk/test used to run the type's own validate and 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: createStandardSchema takes the type as a third argument and reports type-level issues through its result.

That also fixes how tailor seed validate reports 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 createTailorDBHook directly to assert a type-level validate throws needs to go through createStandardSchema instead.

…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-bot

changeset-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9da2117

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@tailor-platform/sdk-plugin-seed Major
@tailor-platform/sdk Minor
@tailor-platform/create-sdk Minor
@tailor-platform/sdk-plugin-tailordb-erd Major

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

@pkg-pr-new

pkg-pr-new Bot commented Aug 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tailor-platform/create-sdk

pnpm add https://pkg.pr.new/tailor-platform/sdk/@tailor-platform/create-sdk@9da2117

@tailor-platform/eslint-plugin-sdk

pnpm add https://pkg.pr.new/tailor-platform/sdk/@tailor-platform/eslint-plugin-sdk@9da2117

@tailor-platform/sdk

pnpm add https://pkg.pr.new/tailor-platform/sdk/@tailor-platform/sdk@9da2117

@tailor-platform/sdk-plugin-seed

pnpm add https://pkg.pr.new/tailor-platform/sdk/@tailor-platform/sdk-plugin-seed@9da2117

@tailor-platform/sdk-plugin-tailordb-erd

pnpm add https://pkg.pr.new/tailor-platform/sdk/@tailor-platform/sdk-plugin-tailordb-erd@9da2117

commit: 9da2117

@github-actions

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.
@github-actions

This comment has been minimized.

This comment was marked as resolved.

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.
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

This comment was marked as off-topic.

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.
@github-actions

This comment has been minimized.

This comment was marked as resolved.

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.
@github-actions

This comment has been minimized.

This comment was marked as resolved.

…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.
@github-actions

This comment has been minimized.

This comment was marked as resolved.

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.
@github-actions

This comment has been minimized.

This comment was marked as resolved.

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.
@github-actions

This comment has been minimized.

This comment was marked as resolved.

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
toiroakr force-pushed the feat/seed-fill-ids branch from dfb9676 to 9da2117 Compare August 5, 2026 13:16
@toiroakr
toiroakr removed the request for review from Copilot August 5, 2026 13:17
@toiroakr toiroakr removed their assignment Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Code Metrics Report (packages/sdk)

main (542c5e9) #1975 (635524c) +/-
Coverage 78.5% 78.5% +0.0%
Code to Test Ratio 1:0.4 1:0.4 -0.1
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%)

Files Coverage +/- Status
packages/sdk/src/plugin/builtin/seed/idp-user-processor.ts 100.0% 0.0% modified
packages/sdk/src/plugin/builtin/seed/lines-db-processor.ts 32.1% 0.0% modified
packages/sdk/src/utils/test/index.ts 95.7% +0.7% modified

SDK Configure Bundle Size

main (542c5e9) #1975 (635524c) +/-
configure-index-size 39.25KB 39.25KB 0KB
dependency-chunks-size 24.96KB 24.96KB 0KB
total-bundle-size 64.22KB 64.22KB 0KB

Runtime Performance

main (542c5e9) #1975 (635524c) +/-
Generate Median 1,815ms 2,113ms 298ms
Generate Max 1,907ms 2,283ms 376ms
Apply Build Median 1,826ms 2,147ms 321ms
Apply Build Max 1,843ms 2,175ms 332ms

Type Performance (instantiations)

main (542c5e9) #1975 (635524c) +/-
tailordb-basic 40,450 40,450 0
tailordb-optional 4,535 4,535 0
tailordb-relation 3,854 3,854 0
tailordb-validate 666 666 0
tailordb-hooks 5,499 5,499 0
tailordb-object 16,345 16,345 0
tailordb-enum 1,538 1,538 0
resolver-basic 12,120 12,120 0
resolver-nested 35,419 35,419 0
resolver-array 24,056 24,056 0
executor-schedule 4,489 4,489 0
executor-webhook 1,130 1,130 0
executor-record 4,542 4,542 0
executor-resolver 5,808 5,808 0
executor-operation-function 1,118 1,118 0
executor-operation-gql 1,126 1,126 0
executor-operation-webhook 1,137 1,137 0
executor-operation-workflow 1,931 1,931 0

Reported by octocov

@toiroakr toiroakr assigned dqn and unassigned dqn Aug 5, 2026

@dqn dqn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will fix it separately.

@toiroakr
toiroakr merged commit 185db9d into main Aug 7, 2026
50 checks passed
@toiroakr
toiroakr deleted the feat/seed-fill-ids branch August 7, 2026 01:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants