Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b5ca25b
feat(seed): add `tailor seed fill` to fill in create-time values for …
toiroakr Aug 3, 2026
0b37129
test(seed): assert a filled id with z.uuid() instead of a hand-rolled…
toiroakr Aug 4, 2026
5465ac6
fix(seed): stop `seed fill` from dropping keys and writing nulls
toiroakr Aug 4, 2026
f3b3c0f
test(seed): cover a `__proto__` key surviving the key ordering
toiroakr Aug 4, 2026
f7e9823
feat(create-sdk): scaffold a `seed:fill` script next to `seed:validate`
toiroakr Aug 4, 2026
ff41e77
docs(seed): say values are preserved, not the bytes of a rewritten line
toiroakr Aug 4, 2026
f4734df
fix(seed): build seed data paths with pathe so they do not vary by pl…
toiroakr Aug 4, 2026
3e559a4
perf(seed): stop reading a table's JSONL when it has no field to fill
toiroakr Aug 4, 2026
3396643
feat(seed): fill from the type's create hooks instead of validating f…
toiroakr Aug 4, 2026
b0d3b80
fix(seed): read every seed hook before the fill writes anything
toiroakr Aug 4, 2026
2da69c9
docs(seed): drop the library name from validateSeedData's description
toiroakr Aug 4, 2026
1bba7d9
fix(seed): keep the fill's hook from validating, and never write a ba…
toiroakr Aug 5, 2026
4f6c5b5
fix(seed): judge a produced value by its contents, not its key count
toiroakr Aug 5, 2026
e900b41
refactor(test): validate a seed row where the field schema already does
toiroakr Aug 5, 2026
aba299a
fix(seed): stop `seed validate` printing a marker in front of the report
toiroakr Aug 5, 2026
e0f028c
docs(seed): drop the claim that the fill validates before it writes
toiroakr Aug 5, 2026
ec03ad7
Merge remote-tracking branch 'origin/main' into feat/seed-fill-ids
toiroakr Aug 5, 2026
a617ce1
fix(seed): write a filled field as an own property
toiroakr Aug 5, 2026
cab0700
feat(seed): name the seed lines the fill could not read
toiroakr Aug 5, 2026
6a781a4
chore(example): regenerate the generator expected fixtures
toiroakr Aug 5, 2026
9da2117
fix(seed): list the seed tables in a stable order
toiroakr Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .changeset/seed-fill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
"@tailor-platform/sdk-plugin-seed": minor
"@tailor-platform/sdk": minor
"@tailor-platform/create-sdk": minor
---

Add `tailor seed fill` to fill in the values a record gets on create for the JSONL seed data rows that are missing them. It fills `id` by default, so rows can reference each other by id, and `--fields` names any other create-time field:

```bash
# ./seed/data/Customer.jsonl: {"name":"Acme Corporation"}
tailor seed fill
# ./seed/data/Customer.jsonl: {"id":"0b6b6f5e-...","name":"Acme Corporation"}

# also stamp a creation time on rows that have none
tailor seed fill --fields id,createdAt
```

The values come from the type itself — its `id`, its field defaults, its create hooks — applied to each row on its own. Nothing is validated, so a row can be filled while a required field is still missing or while another file references an id that does not exist yet; that is the point, since the ids are what you need in order to write the rows that reference them. Run `tailor seed validate` when 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 line that gains nothing is left byte for byte as it was; a line that takes a value is written with its keys in the order the type declares its fields, so a filled-in `id` lands at the front. A field the type gives no value to is skipped, so `--fields id` covers a whole data directory and leaves the IdP `_User` data alone, and naming a field the platform assigns — a `serial` field, for instance — fills nothing and says so.

`tailor seed apply --upsert` now points at the command when a row has no `id`, since that is the run it blocks, and newly scaffolded projects get a `seed:fill` script next to `seed:validate`.

The generated seed schema files now export the type's create hook, which is where the values come from. Run `tailor generate` after upgrading; until then `tailor seed fill` reports which file needs regenerating.

`createTailorDBHook` from `@tailor-platform/sdk/test` no longer runs the type's own `validate`. Computing the values a record gets on create and deciding whether a record is acceptable are separate jobs, and the second one now sits where the field-level validation already was: `createStandardSchema` takes the type as a third argument and reports type-level issues through its result.

`tailor seed validate` also stops printing two markers on the header of a failed run (`\u2716 \u2717 Found 2 error(s) in ...`), now that it hands the CLI a report that is already formatted.

That also fixes how `tailor seed validate` reports them. A type-level `validate` failure used to end the run at the first offending row with a bare message; it now lands in the same report as every other issue, naming the file and every row that fails.

A test calling `createTailorDBHook` directly to assert a type-level `validate` throws needs to go through `createStandardSchema` instead.

The same operation is available as `fillSeedData` from `@tailor-platform/sdk/seed`.
4 changes: 2 additions & 2 deletions example/seed/data/Customer.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const schemaType = t.object({
...customer.omitFields(["id"]),
});

const hook = createTailorDBHook(customer);
export const hook = createTailorDBHook(customer);

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
createStandardSchema(schemaType, hook, customer),
);
4 changes: 2 additions & 2 deletions example/seed/data/Event.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const schemaType = t.object({
...event.omitFields(["id"]),
});

const hook = createTailorDBHook(event);
export const hook = createTailorDBHook(event);

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
createStandardSchema(schemaType, hook, event),
);
4 changes: 2 additions & 2 deletions example/seed/data/Invoice.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const schemaType = t.object({
...invoice.omitFields(["id","invoiceNumber","sequentialId"]),
});

const hook = createTailorDBHook(invoice);
export const hook = createTailorDBHook(invoice);

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
createStandardSchema(schemaType, hook, invoice),
{
foreignKeys: [
{"column":"salesOrderID","references":{"table":"SalesOrder","column":"id"}},
Expand Down
4 changes: 2 additions & 2 deletions example/seed/data/NestedProfile.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const schemaType = t.object({
...nestedProfile.omitFields(["id"]),
});

const hook = createTailorDBHook(nestedProfile);
export const hook = createTailorDBHook(nestedProfile);

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
createStandardSchema(schemaType, hook, nestedProfile),
);
4 changes: 2 additions & 2 deletions example/seed/data/ProductBundle.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const schemaType = t.object({
...productBundle.omitFields(["id"]),
});

const hook = createTailorDBHook(productBundle);
export const hook = createTailorDBHook(productBundle);

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
createStandardSchema(schemaType, hook, productBundle),
);
4 changes: 2 additions & 2 deletions example/seed/data/PurchaseOrder.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const schemaType = t.object({
...purchaseOrder.omitFields(["id"]),
});

const hook = createTailorDBHook(purchaseOrder);
export const hook = createTailorDBHook(purchaseOrder);

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
createStandardSchema(schemaType, hook, purchaseOrder),
{
foreignKeys: [
{"column":"supplierID","references":{"table":"Supplier","column":"id"}},
Expand Down
4 changes: 2 additions & 2 deletions example/seed/data/SalesOrder.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const schemaType = t.object({
...salesOrder.omitFields(["id"]),
});

const hook = createTailorDBHook(salesOrder);
export const hook = createTailorDBHook(salesOrder);

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
createStandardSchema(schemaType, hook, salesOrder),
{
foreignKeys: [
{"column":"customerID","references":{"table":"Customer","column":"id"}},
Expand Down
4 changes: 2 additions & 2 deletions example/seed/data/SalesOrderCreated.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const schemaType = t.object({
...salesOrderCreated.omitFields(["id"]),
});

const hook = createTailorDBHook(salesOrderCreated);
export const hook = createTailorDBHook(salesOrderCreated);

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
createStandardSchema(schemaType, hook, salesOrderCreated),
);
4 changes: 2 additions & 2 deletions example/seed/data/Selfie.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const schemaType = t.object({
...selfie.omitFields(["id"]),
});

const hook = createTailorDBHook(selfie);
export const hook = createTailorDBHook(selfie);

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
createStandardSchema(schemaType, hook, selfie),
{
foreignKeys: [
{"column":"parentID","references":{"table":"Selfie","column":"id"}},
Expand Down
4 changes: 2 additions & 2 deletions example/seed/data/Supplier.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const schemaType = t.object({
...supplier.omitFields(["id"]),
});

const hook = createTailorDBHook(supplier);
export const hook = createTailorDBHook(supplier);

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
createStandardSchema(schemaType, hook, supplier),
);
4 changes: 2 additions & 2 deletions example/seed/data/User.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const schemaType = t.object({
...user.omitFields(["id"]),
});

const hook = createTailorDBHook(user);
export const hook = createTailorDBHook(user);

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
createStandardSchema(schemaType, hook, user),
{
foreignKeys: [
{"column":"email","references":{"table":"_User","column":"name"}},
Expand Down
4 changes: 2 additions & 2 deletions example/seed/data/UserLog.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const schemaType = t.object({
...userLog.omitFields(["id"]),
});

const hook = createTailorDBHook(userLog);
export const hook = createTailorDBHook(userLog);

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
createStandardSchema(schemaType, hook, userLog),
{
foreignKeys: [
{"column":"userID","references":{"table":"User","column":"id"}},
Expand Down
4 changes: 2 additions & 2 deletions example/seed/data/UserSetting.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const schemaType = t.object({
...userSetting.omitFields(["id"]),
});

const hook = createTailorDBHook(userSetting);
export const hook = createTailorDBHook(userSetting);

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
createStandardSchema(schemaType, hook, userSetting),
{
foreignKeys: [
{"column":"userID","references":{"table":"User","column":"id"}},
Expand Down
2 changes: 1 addition & 1 deletion example/seed/data/_User.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const schemaType = t.object({
});

// Simple identity hook for _User (no TailorDB backing type)
const hook = <T>(data: unknown) => data as T;
export const hook = <T>(data: unknown) => data as T;

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
Expand Down
1 change: 1 addition & 0 deletions packages/create-sdk/templates/generators/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"generate": "tailor generate",
"deploy": "tailor deploy",
"seed": "tailor seed apply",
"seed:fill": "tailor seed fill",
"seed:validate": "tailor seed validate",
"test": "vitest --project unit",
"test:unit": "vitest --project unit",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const schemaType = t.object({
...category.omitFields(["id"]),
});

const hook = createTailorDBHook(category);
export const hook = createTailorDBHook(category);

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
createStandardSchema(schemaType, hook, category),
{
foreignKeys: [
{"column":"parentCategoryId","references":{"table":"Category","column":"id"}},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const schemaType = t.object({
...order.omitFields(["id"]),
});

const hook = createTailorDBHook(order);
export const hook = createTailorDBHook(order);

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
createStandardSchema(schemaType, hook, order),
{
foreignKeys: [
{"column":"productId","references":{"table":"Product","column":"id"}},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const schemaType = t.object({
...product.omitFields(["id"]),
});

const hook = createTailorDBHook(product);
export const hook = createTailorDBHook(product);

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
createStandardSchema(schemaType, hook, product),
{
foreignKeys: [
{"column":"categoryId","references":{"table":"Category","column":"id"}},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const schemaType = t.object({
...user.omitFields(["id"]),
});

const hook = createTailorDBHook(user);
export const hook = createTailorDBHook(user);

export const schema = defineSchema(
createStandardSchema(schemaType, hook),
createStandardSchema(schemaType, hook, user),
{
indexes: [
{"name":"user_email_unique_idx","columns":["email"],"unique":true},
Expand Down
50 changes: 48 additions & 2 deletions packages/sdk-plugin-seed/README.md

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.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @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`, and validate that data against the generated schemas.
Tailor CLI plugin that provides the `tailor seed` commands: seed TailorDB (and IdP `_User`) data from JSONL files generated by `seedPlugin`, 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-seed` executable that the Tailor CLI dispatches to when you run `tailor seed`. Keep `seedPlugin` from `@tailor-platform/sdk/plugin/seed` in `definePlugins()` — it generates the seed data and schema files this plugin consumes.
Expand Down Expand Up @@ -34,11 +34,57 @@ tailor seed apply User Order
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
```

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 type requires), 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.
## 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:

```jsonl
# ./seed/data/Customer.jsonl before
{"name":"Acme Corporation","email":"contact@acme.com"}
# after
{"id":"0b6b6f5e-3b8a-4d0e-9a1f-2c7d8e5a4b31","name":"Acme Corporation","email":"contact@acme.com"}
```

`--fields` 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:

```bash
tailor seed fill --fields id,createdAt
```

The values come from the type 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 type 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 type — a `serial` field — has nothing to fill in from here:

```
⚠ No seed data produces a value for: invoiceNumber
✓ Nothing to fill
```

**A 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 type 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 type 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 type 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 type 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:

Expand Down
Loading
Loading