@tailor-platform/sdk 2.26.0 → 2.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/dist/application-DFeuELRo.mjs +200 -0
  3. package/dist/application-DFeuELRo.mjs.map +1 -0
  4. package/dist/application-nT_4a5HO.mjs +1 -0
  5. package/dist/cli/commands/deploy/deploy.d.mts +2 -0
  6. package/dist/cli/commands/deploy/tailordb/migration-validation.d.mts +2 -1
  7. package/dist/cli/commands/tailordb/migrate/bundler.d.mts +4 -6
  8. package/dist/cli/commands/tailordb/migrate/diff-calculator.d.mts +6 -0
  9. package/dist/cli/commands/tailordb/migrate/remote-state.d.mts +1 -0
  10. package/dist/cli/lib.d.mts +2 -2
  11. package/dist/cli/lib.mjs +1 -1
  12. package/dist/cli/lib.mjs.map +1 -1
  13. package/dist/cli/main.mjs +54 -54
  14. package/dist/cli/main.mjs.map +1 -1
  15. package/dist/cli/shared/workspace-name.d.mts +2 -0
  16. package/dist/completion/zsh-worker.zsh +4 -2
  17. package/dist/configure/config/types.d.mts +11 -0
  18. package/dist/configure/index.d.mts +6 -6
  19. package/dist/configure/index.mjs +1 -1
  20. package/dist/configure/index.mjs.map +1 -1
  21. package/dist/configure/services/auth/types.d.mts +6 -1
  22. package/dist/configure/services/executor/trigger/event.d.mts +1 -1
  23. package/dist/configure/services/executor/trigger/webhook.d.mts +1 -1
  24. package/dist/configure/services/idp/permission.d.mts +2 -2
  25. package/dist/configure/services/resolver/resolver.d.mts +1 -1
  26. package/dist/configure/services/tailordb/schema.d.mts +1 -1
  27. package/dist/configure/services/tailordb/types.d.mts +1 -1
  28. package/dist/configure/services/workflow/job.d.mts +1 -1
  29. package/dist/configure/types/field.types.d.mts +48 -11
  30. package/dist/configure/types/type.d.mts +1 -1
  31. package/dist/date-CgP7gMG_.mjs +2 -0
  32. package/dist/date-CgP7gMG_.mjs.map +1 -0
  33. package/dist/{secretmanager-5olfnI1b.mjs → date-representation-Cr7W7Pn2.mjs} +2 -2
  34. package/dist/date-representation-Cr7W7Pn2.mjs.map +1 -0
  35. package/dist/{field-parse-DhnFs9WG.mjs → field-parse-C41e3UAF.mjs} +2 -2
  36. package/dist/{field-parse-DhnFs9WG.mjs.map → field-parse-C41e3UAF.mjs.map} +1 -1
  37. package/dist/idp-G_ojPBB5.mjs.map +1 -1
  38. package/dist/{manager-8-JOvuLl.mjs → manager-Bk-7vkwd.mjs} +2 -2
  39. package/dist/{manager-8-JOvuLl.mjs.map → manager-Bk-7vkwd.mjs.map} +1 -1
  40. package/dist/migration-steps-C6F3zNdQ.mjs +3 -0
  41. package/dist/migration-steps-C6F3zNdQ.mjs.map +1 -0
  42. package/dist/plugin/index.mjs +1 -1
  43. package/dist/register-ts-hook-D6KXrGrB.mjs +1009 -0
  44. package/dist/register-ts-hook-D6KXrGrB.mjs.map +1 -0
  45. package/dist/runtime/idp.d.mts +6 -0
  46. package/dist/runtime/index.mjs +1 -1
  47. package/dist/runtime/types.d.mts +5 -0
  48. package/dist/{schema-CRtAmDcl.mjs → schema-B6hJQWp6.mjs} +2 -2
  49. package/dist/{schema-CRtAmDcl.mjs.map → schema-B6hJQWp6.mjs.map} +1 -1
  50. package/dist/seed/index.d.mts +3 -1
  51. package/dist/seed/index.mjs +7 -9
  52. package/dist/seed/index.mjs.map +1 -1
  53. package/dist/service-Cc7OY0Oh.mjs +7 -0
  54. package/dist/service-Cc7OY0Oh.mjs.map +1 -0
  55. package/dist/service-sH2MZ4_W.mjs +1 -0
  56. package/dist/service_pb-Dw2ilc1O.mjs +2 -0
  57. package/dist/{service_pb-ZClWtj1X.mjs.map → service_pb-Dw2ilc1O.mjs.map} +1 -1
  58. package/dist/service_pb-GDOqfwb6.mjs +1 -0
  59. package/dist/types/field.generated.d.mts +2 -2
  60. package/dist/utils/test/index.mjs +1 -1
  61. package/dist/vitest/date-representation.d.mts +23 -0
  62. package/dist/vitest/index.d.mts +3 -1
  63. package/dist/vitest/index.mjs +1 -1
  64. package/dist/vitest/index.mjs.map +1 -1
  65. package/dist/vitest/migration-steps.d.mts +45 -0
  66. package/dist/vitest/mocks/file.d.mts +1 -1
  67. package/dist/vitest/mocks/idp.d.mts +1 -0
  68. package/dist/vitest/setup.d.mts +3 -1
  69. package/dist/vitest/setup.mjs +1 -1
  70. package/dist/vitest/setup.mjs.map +1 -1
  71. package/dist/{workspace_resource_pb-CxR_6fyN.mjs → workspace_resource_pb-ChWejcj0.mjs} +2 -2
  72. package/dist/workspace_resource_pb-ChWejcj0.mjs.map +1 -0
  73. package/docs/cli/application.md +24 -16
  74. package/docs/cli/function.md +2 -0
  75. package/docs/cli/workspace.md +1 -1
  76. package/docs/configuration.md +19 -6
  77. package/docs/github-actions.md +124 -58
  78. package/docs/plugin/custom.md +2 -1
  79. package/docs/services/auth.md +20 -0
  80. package/docs/services/executor.md +2 -2
  81. package/docs/services/idp.md +6 -1
  82. package/docs/services/resolver.md +3 -1
  83. package/docs/services/staticwebsite.md +5 -5
  84. package/docs/services/tailordb-migration.md +157 -35
  85. package/docs/services/tailordb.md +2 -2
  86. package/docs/testing.md +3 -0
  87. package/package.json +12 -12
  88. package/dist/application-ChqHuhZW.mjs +0 -1
  89. package/dist/application-DS0XKBtK.mjs +0 -200
  90. package/dist/application-DS0XKBtK.mjs.map +0 -1
  91. package/dist/date-Dri6Yas-.mjs +0 -2
  92. package/dist/date-Dri6Yas-.mjs.map +0 -1
  93. package/dist/register-ts-hook-BuwkLuxC.mjs +0 -925
  94. package/dist/register-ts-hook-BuwkLuxC.mjs.map +0 -1
  95. package/dist/secretmanager-5olfnI1b.mjs.map +0 -1
  96. package/dist/service-Bx-7Lz06.mjs +0 -1
  97. package/dist/service-CL1Bp4It.mjs +0 -7
  98. package/dist/service-CL1Bp4It.mjs.map +0 -1
  99. package/dist/service_pb-ZClWtj1X.mjs +0 -2
  100. package/dist/service_pb-dB3RrGeh.mjs +0 -1
  101. package/dist/workspace_resource_pb-CxR_6fyN.mjs.map +0 -1
@@ -97,15 +97,19 @@ A typical change cycle:
97
97
  3. **Edit `migrate.ts`** to populate data for the new required field:
98
98
 
99
99
  ```typescript
100
- import type { Transaction } from "./db";
100
+ import type { MigrationSteps, Transaction } from "./db";
101
101
 
102
- export async function main(trx: Transaction): Promise<void> {
102
+ async function populateUserEmail(trx: Transaction): Promise<void> {
103
103
  await trx
104
104
  .updateTable("User")
105
105
  .set({ email: "default@example.com" })
106
106
  .where("email", "is", null)
107
107
  .execute();
108
108
  }
109
+
110
+ export const steps = {
111
+ populateUserEmail: { run: populateUserEmail },
112
+ } satisfies MigrationSteps;
109
113
  ```
110
114
 
111
115
  4. **Apply.**
@@ -236,7 +240,7 @@ Sometimes existing data must be transformed without any schema change — fixing
236
240
  tailor tailordb migration generate --data-only --name "normalize legacy phone numbers"
237
241
  ```
238
242
 
239
- This writes a numbered migration with an empty `diff.json`, a `migrate.ts` skeleton, and `db.ts` typed against the current schema. Edit `migrate.ts` to implement the transformation; the next `tailor deploy` runs it like any other migration script — in a single transaction, advancing the migration checkpoint (see [Performance and Large Tables](#performance-and-large-tables) for batching patterns). Because the entry is part of the migration history, the fix is versioned, ordered relative to schema changes, and applied once per workspace.
243
+ This writes a numbered migration with an empty `diff.json`, a `migrate.ts` skeleton (a single `migrate` step), and `db.ts` typed against the current schema. Edit `migrate.ts` to implement the transformation; the next `tailor deploy` runs it like any other migration script — in a single transaction, advancing the migration checkpoint (see [Performance and Large Tables](#performance-and-large-tables) for batching patterns). Because the entry is part of the migration history, the fix is versioned, ordered relative to schema changes, and applied once per workspace.
240
244
 
241
245
  The command requires a clean state: if the namespace has schema changes that are not yet in migration files, generate the schema migration first. With multiple namespaces, pass `--namespace` to name the target. `--data-only` cannot be combined with `--init`, `--rename`, `--drop`, or `--expand-contract`.
242
246
 
@@ -268,21 +272,21 @@ export default defineConfig({
268
272
 
269
273
  ## Generated Files
270
274
 
271
- | File | When generated | Description |
272
- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
273
- | `0000/schema.json` | First `migration generate` | Full snapshot of all tables in the namespace. |
274
- | `XXXX/diff.json` | Every subsequent migration | Field-level diff against the previous snapshot. |
275
- | `XXXX/migrate.ts` | Auto-generated for breaking changes and `--data-only` migrations; added manually via `tailordb migration script` for warning-tier changes | Data transformation script. The `main` export receives a Kysely `Transaction`. |
276
- | `XXXX/db.ts` | Generated once when `migrate.ts` is created | Kysely types reflecting the schema **before** this migration. Exports `Database`, `Transaction`, and `MigrationContext`. |
277
- | `XXXX/db.pglite.ts` | Generated with `db.ts` | `CREATE TABLE` script of the same schema, for running `migrate.ts` on PGlite. Never deployed. |
278
- | `XXXX/migrate.test.ts` | Added via `tailordb migration script --with-test` | Unit-test scaffold for `migrate.ts` (see [Testing Migrations Locally](#testing-migrations-locally)). Never deployed. |
279
- | `XXXX/migrate.pglite.test.ts` | Added via `tailordb migration script --with-test` when `@electric-sql/pglite` is installed | PGlite test scaffold for `migrate.ts`. Never deployed. |
275
+ | File | When generated | Description |
276
+ | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
277
+ | `0000/schema.json` | First `migration generate` | Full snapshot of all tables in the namespace. |
278
+ | `XXXX/diff.json` | Every subsequent migration | Field-level diff against the previous snapshot. |
279
+ | `XXXX/migrate.ts` | Auto-generated for breaking changes and `--data-only` migrations; added manually via `tailordb migration script` for warning-tier changes | Data transformation script. The `main` export receives a Kysely `Transaction`; a `steps` export runs each step in its own transaction. |
280
+ | `XXXX/db.ts` | Generated once when `migrate.ts` is created | Kysely types reflecting the schema **before** this migration. Exports `Database`, `Transaction`, and `MigrationContext`. |
281
+ | `XXXX/db.pglite.ts` | Generated with `db.ts` | `CREATE TABLE` script of the same schema, for running `migrate.ts` on PGlite. Never deployed. |
282
+ | `XXXX/migrate.test.ts` | Added via `tailordb migration script --with-test` | Unit-test scaffold for `migrate.ts` (see [Testing Migrations Locally](#testing-migrations-locally)). Never deployed. |
283
+ | `XXXX/migrate.pglite.test.ts` | Added via `tailordb migration script --with-test` when `@electric-sql/pglite` is installed | PGlite test scaffold for `migrate.ts`. Never deployed. |
280
284
 
281
285
  `db.ts` reflects the pre-migration schema because the script runs after the pre-migration phase has temporarily relaxed breaking constraints (e.g., a new `required` field is added as `optional` first), so the data being read still matches the previous shape.
282
286
 
283
287
  ### Migration file format compatibility
284
288
 
285
- Migration files are versioned independently of the SDK package. This SDK writes format version `7` and reads versions `1` through `7`. It normalizes supported older formats in memory; it never rewrites applied migration files on disk. Format version `6` records renames of members inside nested fields (`memberRenames`); older SDK versions refuse to read it rather than deploying such a migration without the copy step. Format version `7` records whether a migration's `db.ts` uses Temporal column types (`temporal`); older SDK versions refuse to read it rather than running the script with `Date` values.
289
+ Migration files are versioned independently of the SDK package. This SDK writes format version `8` and reads versions `1` through `8`. It normalizes supported older formats in memory; it never rewrites applied migration files on disk. Format version `6` records renames of members inside nested fields (`memberRenames`); older SDK versions refuse to read it rather than deploying such a migration without the copy step. Format version `7` records whether a migration's `db.ts` uses Temporal column types (`temporal`); older SDK versions refuse to read it rather than running the script with `Date` values. Format version `8` records the `defaultDateRepresentation` a migration script was generated under (`dateRepresentation`); older SDK versions refuse to read it rather than running the script with string values.
286
290
 
287
291
  Supported histories also preserve the behavior of field hooks and validators saved by older SDKs, including access to the record and boolean validators with a separate error message. Legacy update hooks retain existing values for omitted fields; explicitly supplied values, including `null`, take precedence. This applies to both snapshots and diffs, including nested fields. Your existing migration files can remain as generated.
288
292
 
@@ -294,6 +298,8 @@ There is no migration-file conversion command. Keeping applied files unchanged p
294
298
 
295
299
  ## Migration Script Anatomy
296
300
 
301
+ `migration generate` and `migration script` scaffold `migrate.ts` as `steps`, with one step for each schema change that needs a data migration (see [Splitting a migration into steps](#splitting-a-migration-into-steps)). The examples in this section use `main`, the single-transaction form, which still works. A `steps` script receives the same transaction and `MigrationContext` in each step's `run`.
302
+
297
303
  ```typescript
298
304
  import type { Transaction } from "./db";
299
305
 
@@ -365,6 +371,85 @@ The `env` values are injected at bundle time (the same mechanism as resolvers/ex
365
371
  - `console.log` / `console.error` output is captured and surfaced under `Logs:` in the apply output. Use it sparingly for progress markers on long-running migrations.
366
372
  - The script is idempotency-friendly by default because it runs inside a transaction, but **plan for re-execution**: if a later migration in the same `apply` fails, the platform may retry the apply, and you may want your script to tolerate already-migrated rows (e.g., `where("email", "is", null)` instead of unconditional updates).
367
373
 
374
+ ### Splitting a migration into steps
375
+
376
+ A script that exports `main` runs as one job in one transaction. When the work is too large for one job, or splits naturally into independent parts, export `steps` instead. Each step runs as its own job in its own transaction, so each step gets the job's execution-time limit to itself and commits on its own.
377
+
378
+ ```typescript
379
+ import type { MigrationSteps } from "./db";
380
+
381
+ export const steps = {
382
+ backfillCustomer: {
383
+ run: async (trx) => {
384
+ await trx
385
+ .updateTable("Customer")
386
+ .set((eb) => ({ updatedAt: eb.ref("createdAt") }))
387
+ .where("updatedAt", "is", null)
388
+ .execute();
389
+ },
390
+ },
391
+ backfillInvoice: {
392
+ run: async (trx) => {
393
+ await trx
394
+ .updateTable("Invoice")
395
+ .set((eb) => ({ updatedAt: eb.ref("createdAt") }))
396
+ .where("updatedAt", "is", null)
397
+ .execute();
398
+ },
399
+ },
400
+ recomputeTotals: {
401
+ dependsOn: ["backfillInvoice"],
402
+ run: async (trx, { env }) => {
403
+ // Runs only after backfillInvoice has completed.
404
+ },
405
+ },
406
+ } satisfies MigrationSteps;
407
+ ```
408
+
409
+ - A script exports either `main` or `steps`. If it exports both, `main` runs as before and `steps` is ignored with a warning from `tailor deploy` and `tailor tailordb migration validate`.
410
+ - Generated scripts export `steps`. Each schema change that needs a data migration gets its own step, named for what it does: `populate<Table><Field>` for an added required field (for example `populateUserEmail`), `rename<Table><Field>` for a renamed field, `copy<Old>To<New>` for a renamed table, `update<Table><Field>` for a changed field, `resolve<Table><Index>` for a unique index, and `convert<Table><Field>` for a field type conversion. When that name is not a valid step name (for example because it is longer than 64 characters), or when another step or a helper in the script already uses it, the step is named `change<N>` instead (`expand<N>` for a conversion through a temporary field), where N is the change's position in the migration; the fallback does not mean the schema-derived name was wrong. A step lists in `dependsOn` every earlier step that touches the same field (a foreign key that changes its target also touches `id` of the table it now points to), so steps that touch the same field run in the order the changes are listed and changes to different fields do not wait for each other. The copy of a renamed table's rows runs after the copies of the renamed tables its foreign keys point to, so the rows they reference already exist. It skips the rows whose ids the new table already has, so running it again does not insert them twice. A step that rewrites values that already exist (the duplicate resolution of a unique index or field, a field change, a conversion through a temporary field) has a comment above its function saying so: once it commits it cannot be undone, so check the values it writes before you deploy. Where the scaffold cannot pick a value for you (what an added required field holds in existing records, the new value of duplicates, the value that replaces a removed enum value, the new reference of a retargeted foreign key, the copy of renamed tables that reference each other), it calls `TODO(message)`, which `./db` exports. The migration fails at that call, and `tailordb migration validate` and `tailor deploy` reject a `migrate.ts` that still calls it before anything is changed. Replace each call with the value, then remove the `TODO` import. A `--data-only` migration, and a migration with no data statements to scaffold, get a single `migrate` step; split it into several steps where the work divides.
411
+ - Each step is an object with a `run` function — it receives its own transaction and the same `MigrationContext` as `main` — and an optional `dependsOn` list naming the steps that must complete before it starts.
412
+ - Steps with no dependency between them have no guaranteed order. Declaration order is not execution order, and a future SDK version may run independent steps concurrently, so declare `dependsOn` for every ordering your steps rely on.
413
+ - Declare `steps` directly as `export const steps = { ... }`, with each step written inside it as an object literal with a literal name and `dependsOn` list. Step names start with a letter and contain only letters, digits, and underscores, up to 64 characters. `tailor tailordb migration validate` and `tailor deploy` reject unknown dependencies and cycles before anything is changed.
414
+ - Each step runs as a separate job, so steps cannot hand data to each other through module-level variables; read what a step needs from the database.
415
+ - `MigrationSteps` is declared by `db.ts` files generated by this SDK version. With an older `db.ts`, annotate each `run` parameter with `Transaction` instead.
416
+
417
+ A script with a single step runs like a `main` script: one job and one transaction, and a failure inside the transaction rolls its data changes back. Nothing is recorded as in progress. If the post-migration schema changes fail after the transaction committed, the schema is restored to its state before the migration but the data changes stay, so the next deploy runs the step again and the step must tolerate rows it already migrated. The rules below apply once the script has two or more steps. A migration that an earlier deploy already left in progress keeps running step by step until it completes, even if the script has since been reduced to one step.
418
+
419
+ Because steps commit separately, a failure does not undo the steps that already completed:
420
+
421
+ - If the first step fails before any other step has completed, the migration fails like a `main` script: the pre-migration schema changes are rolled back.
422
+ - If a step fails after another step has completed, `tailor deploy` stops and leaves the migration **in progress**. The pre-migration schema stays in place, the checkpoint is not advanced, and the namespace's tables stay in maintenance mode, as while the migration runs: GraphQL create, update, and delete operations, bulk upsert, and record events remain disabled, while `read` keeps its setting. `tailor tailordb migration status` reports the migration as in progress.
423
+ - If `tailor deploy` cannot confirm whether the steps started, for example because the connection dropped while starting them, the migration also stays in progress. Run `tailor deploy` again; it checks whether the steps ran before continuing.
424
+ - Fix the failing step and run `tailor deploy` again. The deploy resumes the migration: completed steps do not run again, and the failed step and the steps after it run with the updated script. A completed step is treated like an applied migration — editing it afterwards does not run it again.
425
+ - If the steps changed since the earlier run, or that run is no longer available, every step runs again from the beginning. See [Editing steps of a migration in progress](#editing-steps-of-a-migration-in-progress).
426
+ - If every step completed but the post-migration schema changes fail (for example, a field the migration makes required still has empty values), the migration also stays in progress with its pre-migration schema, and the next deploy retries those schema changes without running the steps again.
427
+ - To run a completed step again (because it had a bug, or to fix rows the post-migration schema changes reject), change the steps: rename that step and update the `dependsOn` lists that name it, or add a step that fixes the rows. Every step then runs again from the beginning, so this relies on every step being safe to run again; to keep the other completed steps from running again, skip them as described below.
428
+ - While a migration is in progress, `migration set`, `migration sync`, and `migration rebaseline` refuse to run, and `migration test` refuses to use that workspace as its source. Finish the migration with `tailor deploy` first. The one exception is `migration set <N>` with the in-progress migration's own number: it marks that migration as completed and clears its in-progress record. Use it only when the migration's schema changes are already applied, for example after the deploy reported that the checkpoint write could not be confirmed.
429
+
430
+ #### Editing steps of a migration in progress
431
+
432
+ When `tailor deploy` finds a migration in progress, what you changed in `migrate.ts` decides how it continues:
433
+
434
+ | What you changed since the earlier deploy | What the next `tailor deploy` does |
435
+ | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
436
+ | The code of a step that has not completed (the failed step or a later one) | Resumes: completed steps do not run again, and the rest run with the updated code. |
437
+ | The code of a step that already completed | Resumes, but the change takes no effect: that step does not run again. |
438
+ | A step added, removed, or renamed, or the order the steps run in | Runs every step again from the beginning, and warns about it first. The warning lists the steps that had already succeeded. |
439
+ | Nothing, but the earlier run was canceled or is no longer available | Runs every step again from the beginning, and warns about it first. |
440
+
441
+ Running every step again runs completed steps a second time, which breaks a step that is not safe to run twice. To keep specific steps from running again, pass them to `--migration-skip-steps` as comma-separated `<namespace>/<step>` entries:
442
+
443
+ ```bash
444
+ tailor deploy --migration-skip-steps main-db/backfillUser,main-db/backfillInvoice
445
+ ```
446
+
447
+ - A skipped step is reported as completed and is not run. Only steps that succeeded in the migration's earlier run can be skipped, so a step you renamed cannot be.
448
+ - The option applies to the migration an earlier deploy left in progress. Deploy fails before changing anything when an entry names a step that did not succeed, a step the migration does not define, a namespace without a migration in progress, or a namespace that no deployed config defines. It also fails when the earlier run's step results cannot be read, because then it cannot confirm which steps succeeded; deploy again to retry.
449
+ - It takes effect for the deploy you pass it to. If that run fails, the next `tailor deploy` resumes it as usual, and the skipped steps stay completed without passing the option again.
450
+
451
+ Write every step so that running it again is safe: besides the cases above, a step can run a second time when a deploy is interrupted right after it commits. Use `where` clauses that skip rows a step already migrated, as in the example. Maintenance mode blocks GraphQL writes, not the writes your resolvers, executors, or workflows make through Kysely; rows they write while a migration is in progress are not seen by steps that already completed, so stop such writers until the migration finishes or write steps that tolerate them.
452
+
368
453
  ## Supported Schema Changes
369
454
 
370
455
  | Change Type | Breaking? | Migration Script? | Notes |
@@ -425,9 +510,9 @@ A `float` field that is already unique cannot convert to `decimal` in place, bec
425
510
  For example, changing `User.age` from `integer` to `float` generates a `migrate.ts` that scans non-null values in batches of 100:
426
511
 
427
512
  ```typescript
428
- import type { Transaction } from "./db";
513
+ import { TODO, type MigrationSteps, type Transaction } from "./db";
429
514
 
430
- export async function main(trx: Transaction): Promise<void> {
515
+ async function updateUserAge(trx: Transaction): Promise<void> {
431
516
  // Normalize User.age from integer to float while the previous type is still active
432
517
  {
433
518
  let lastId: string | undefined;
@@ -445,11 +530,11 @@ export async function main(trx: Transaction): Promise<void> {
445
530
  if (rows.length === 0) break;
446
531
 
447
532
  for (const row of rows) {
448
- // TODO(tailor-migration-review): Remove this marker and the `never` annotation after reviewing the normalization.
449
- // Keep the value accepted by the active integer type and castable to float.
450
533
  const sourceValue = row.age;
451
534
  if (sourceValue === null) continue;
452
- const normalizedValue: never = sourceValue;
535
+ const normalizedValue = TODO(
536
+ "normalize User.age to a value the active integer type accepts and the float type can cast",
537
+ );
453
538
  if (Object.is(normalizedValue, sourceValue)) continue;
454
539
  await trx
455
540
  .updateTable("User")
@@ -461,9 +546,13 @@ export async function main(trx: Transaction): Promise<void> {
461
546
  }
462
547
  }
463
548
  }
549
+
550
+ export const steps = {
551
+ updateUserAge: { run: updateUserAge },
552
+ } satisfies MigrationSteps;
464
553
  ```
465
554
 
466
- The generated `never` annotation intentionally causes a TypeScript error until you review the normalization. If the existing values are already suitable for the target type, remove the annotation and review marker to accept the identity transformation; it does not write any rows. If values need application-specific normalization, replace the expression and remove the annotation and marker while keeping the result valid for both the active source type and the target type. The source field contract remains active until the script finishes; for example, an `integer` → `float` script cannot write fractional values during this phase.
555
+ The generated `TODO()` call stops the migration until you replace it, and `tailordb migration validate` and `tailor deploy` reject the migration while it is still there. If the existing values are already suitable for the target type, replace the call with `sourceValue` to accept the identity transformation; it does not write any rows. If values need application-specific normalization, replace it with your expression, keeping the result valid for both the active source type and the target type. The source field contract remains active until the script finishes; for example, an `integer` → `float` script cannot write fractional values during this phase.
467
556
 
468
557
  ### Converting a field type
469
558
 
@@ -476,10 +565,10 @@ User.price changes from string to integer, which cannot be applied in one step.
476
565
 
477
566
  Confirming writes two migrations:
478
567
 
479
- 1. **The conversion.** Adds a temporary field (`priceMigrate`), converts each stored value into it, and clears and removes the original field. Edit the conversion expression before deploying: the generated `never` annotation fails your typecheck, and `tailordb migration validate` rejects the migration while the review marker is still there. When only the array-ness changes (`string` → `string[]`), the conversion stores each value as a one-element array and carries no review marker; when the element type changes as well (`integer` → `string[]`), you convert the element and the script wraps it.
568
+ 1. **The conversion.** Adds a temporary field (`priceMigrate`), converts each stored value into it, and clears and removes the original field. Edit the conversion expression before deploying: the generated `TODO()` call fails the migration, and `tailordb migration validate` and `tailor deploy` reject it while the call is still there. When only the array-ness changes (`string` → `string[]`), the conversion stores each value as a one-element array and has no `TODO()` call; when the element type changes as well (`integer` → `string[]`), you convert the element and the script wraps it.
480
569
  2. **The rename.** Renames the temporary field back to `price`. Its copy script is complete, but this migration also carries every other schema change the same run picked up, so review it as you would any generated migration.
481
570
 
482
- If converting to an array also reduces a decimal field's `scale` or removes enum values, the conversion keeps the review marker. Edit the element conversion to satisfy the target field before deploying.
571
+ If converting to an array also reduces a decimal field's `scale` or removes enum values, the conversion keeps the `TODO()` call. Edit the element conversion to satisfy the target field before deploying.
483
572
 
484
573
  `tailor deploy` applies both. Because the conversion only touches rows whose original value is still set, a re-run resumes where it stopped rather than converting a row twice.
485
574
 
@@ -508,7 +597,7 @@ Some changes are still rejected and need a temporary field you add yourself —
508
597
 
509
598
  The command performs the following sequence:
510
599
 
511
- 1. Reads each migration-enabled namespace's `sdk-migration` checkpoint from the source workspace and reconstructs that exact snapshot from local migration history.
600
+ 1. Reads each migration-enabled namespace's `sdk-migration` checkpoint from the source workspace and reconstructs that exact snapshot from local migration history. A deployed namespace that has no checkpoint is treated as migration 0, as `tailor deploy` does, but only when its schema matches the `0000` snapshot; otherwise the command fails and lists the differences. A namespace that was never deployed also fails.
512
601
  2. Creates a temporary workspace in the same region, organization, and folder as the source, unless `--target-workspace-id` names an existing throwaway workspace.
513
602
  3. Deploys the checkpoint snapshots and writes their checkpoint labels.
514
603
  4. Loads fixture data or clones source records.
@@ -582,7 +671,7 @@ When you run `tailor deploy`, the SDK detects pending migrations (anything past
582
671
  For each pending migration:
583
672
 
584
673
  1. **Pre-migration**: Schema changes that would be breaking are applied in a relaxed form first. A verified in-place field type change keeps its complete previous field contract until Post-migration, including field and table-level hooks or validators changed by the same migration. Newly-required fields are added as optional; fields whose `optional → required` transition is breaking are temporarily kept optional. Fields that are being removed in this migration are temporarily kept on the table so that `migrate.ts` can still read them (for example, to `innerJoin` through a foreign key that is about to be dropped); members removed from a nested field are kept the same way, and the new member of a confirmed nested rename is added as optional and non-unique. For a renamed field, the old field is kept and the new field is added with its constraints relaxed, so the script can read the old field and write the new one. For a renamed table, the new table is created with its full constraints while the old table stays on the namespace until post-migration cleanup, so the script can copy rows between them. Breaking table-level index changes are relaxed the same way: a newly-added unique index is withheld, and an index gaining a unique constraint (or a unique index changing its field set) keeps its previous definition, so `migrate.ts` can resolve duplicates first. Non-breaking changes that are part of the same migration are also applied here.
585
- 2. **Script execution**: If `migrate.ts` exists on disk for this migration, it is bundled and sent to the platform via the script execution API and runs as the configured machine user inside a transaction. The script is hard-required for breaking changes (`diff.requiresMigrationScript`) — deploy fails if the file is missing, unless a `--no-script` acknowledgment was recorded (see [Breaking changes without a script](#breaking-changes-without-a-script)). It is also executed when present for warning-tier diffs — see [Warnings and optional migration scripts](#warnings-and-optional-migration-scripts).
674
+ 2. **Script execution**: If `migrate.ts` exists on disk for this migration, it is bundled and runs on the platform as the configured machine user inside a transaction — or, for a script that exports [`steps`](#splitting-a-migration-into-steps), one transaction per step. The script is hard-required for breaking changes (`diff.requiresMigrationScript`) — deploy fails if the file is missing, unless a `--no-script` acknowledgment was recorded (see [Breaking changes without a script](#breaking-changes-without-a-script)). It is also executed when present for warning-tier diffs — see [Warnings and optional migration scripts](#warnings-and-optional-migration-scripts).
586
675
  3. **Post-migration schema**: Required constraints and the target field definitions are applied. Do not assume that removing a field clears its underlying stored JSON value.
587
676
  4. **Checkpoint and cleanup**: The `sdk-migration` label is bumped to this migration's number, then removed GQL permissions and tables — including the old table left behind by a rename — are deleted. Advancing the checkpoint first prevents a failed checkpoint write from requiring the SDK to recreate irreversibly deleted records.
588
677
 
@@ -608,7 +697,7 @@ Namespace: tailordb
608
697
 
609
698
  The error also points you at `migration status`, `migration generate`, `migration sync`, and `migration set` — see [Remote schema drift detected](#remote-schema-drift-detected) for which one applies.
610
699
 
611
- To run the same checks without deploying — plus migration file integrity (numbering, parseable contents, a `migrate.ts` or a recorded `--no-script` acknowledgment for every migration that requires a script, and no unresolved generated normalization review markers):
700
+ To run the same checks without deploying — plus migration file integrity (numbering, parseable contents, a `migrate.ts` or a recorded `--no-script` acknowledgment for every migration that requires a script, and no `TODO()` call left in a `migrate.ts`):
612
701
 
613
702
  ```bash
614
703
  tailor tailordb migration validate
@@ -728,13 +817,17 @@ Use `tailor tailordb migration rebaseline` rather than combining `migration gene
728
817
 
729
818
  ## Failure Recovery
730
819
 
731
- If the pre-migration phase or `migrate.ts` fails:
820
+ A script that exports `steps` follows different rules once one of its steps has completed: the migration stays in progress and the next deploy resumes it. See [Splitting a migration into steps](#splitting-a-migration-into-steps). Otherwise, if the pre-migration phase or `migrate.ts` fails:
732
821
 
733
822
  - **The transaction rolls back** for that migration's script. Database changes the script made are undone.
734
823
  - **The pre-migration schema changes are rolled back** to the prior checkpoint: tables that already existed are restored to their previous shape, and tables the migration newly introduced are dropped. The workspace is left at its prior checkpoint and prior schema — not half-applied.
735
824
  - The whole `apply` aborts and the checkpoint label is not bumped. Subsequent migrations in the same run do not execute.
736
825
 
737
- The rollback is best-effort per table; if reverting a table fails, a warning is logged and the original migration error is still reported.
826
+ The rollback is best-effort per table; if reverting a table fails, a warning is logged and the original migration error is still reported. The next deploy then stops with remote schema drift on that table. Run `tailor tailordb migration validate` to see which tables differ, return the namespace to its checkpoint with `tailor tailordb migration sync <N>`, where `<N>` is the checkpoint `tailor tailordb migration status` reports, and deploy again. `sync` deletes the tables and fields that are not in that migration's snapshot, together with any rows the failed migration wrote to them; the next deploy runs the failed migration again.
827
+
828
+ If `tailor deploy` cannot tell whether a `main` script ran, for example because the connection dropped while it started or waited for the script, it does not roll anything back: the pre-migration schema changes stay, the namespace's tables stay in maintenance mode, and the deploy reports that it could not confirm the outcome. A later deploy refuses to run the migration while that run is still executing. Run `tailor workflow executions --workflow-name <name>` with the workflow name the error reports until the execution has finished or none is listed. If it succeeded, run `tailor tailordb migration sync <N>` with the migration's own number; otherwise run `tailor tailordb migration sync <N-1>`. Then deploy again. Do not deploy before running sync, even with `--no-schema-check` to skip the drift check: the deploy may run the `main` script again even if the execution succeeded.
829
+
830
+ `migration sync` refuses to run while a migration is in progress. When a steps migration's post-migration schema changes fail and the deploy cannot return its tables to the pre-migration schema, it logs a warning, and the next deploy stops with remote schema drift on those tables. Run `tailor tailordb migration validate` from a checkout of the deployed migration history. If the only issue it reports is that drift, run `tailor deploy --no-schema-check`: the deploy applies the pre-migration schema again, skips the completed steps unless the steps changed, and retries the post-migration schema changes. `--no-schema-check` skips the schema checks of every namespace, so do not use it while `validate` reports any other issue.
738
831
 
739
832
  After a failure:
740
833
 
@@ -791,7 +884,7 @@ The migration script runs in a single transaction. For tables with many rows:
791
884
 
792
885
  - Prefer set-based SQL (`updateTable(...).set(...).where(...)`) over per-row loops.
793
886
  - If a per-row loop is unavoidable, batch by primary key range. Avoid `OFFSET`-based pagination — it scans previously-seen rows on every page.
794
- - Long-running transactions can hit platform timeouts and hold locks. For very large backfills, consider splitting the work across multiple migrations, each operating on a subset.
887
+ - Long-running transactions can hit platform timeouts and hold locks. For very large backfills, consider splitting the work into [steps](#splitting-a-migration-into-steps) or across multiple migrations, each operating on a subset.
795
888
  - Add `LIMIT` and resumability (idempotent `where` clauses) so a re-run after a transient failure converges.
796
889
 
797
890
  ### Migrations that take longer than a minute
@@ -799,12 +892,12 @@ The migration script runs in a single transaction. For tables with many rows:
799
892
  Migration scripts run as workflow jobs, so they are not bound by the 60-second
800
893
  limit that applies to synchronous script execution. `tailor deploy` waits for
801
894
  the migration and reports its logs as usual. Execution time is still bounded —
802
- a workflow job has its own, far longer, execution-time limit.
895
+ a workflow job has its own execution-time limit.
803
896
 
804
- The script still runs in a single transaction, so the guidance above about
805
- locks and transaction size continues to apply — prefer splitting a very large
806
- backfill across several migrations over holding one transaction open for a
807
- long time.
897
+ A `main` script still runs in a single transaction, so the guidance above about
898
+ locks and transaction size continues to apply. When a migration's work does not
899
+ fit one job, split it into [steps](#splitting-a-migration-into-steps): each step
900
+ runs as its own job and transaction.
808
901
 
809
902
  ## Testing Migrations Locally
810
903
 
@@ -844,6 +937,8 @@ Stage the rows each query returns with `mock.enqueueResult(...)` or `mock.setQue
844
937
 
845
938
  These tests need no platform connection and no `tailor-runtime` environment — they run in a plain Vitest setup. Vitest's default `include` pattern already picks up `migrations/**/migrate.test.ts`; if your config narrows `include`, add the migrations directory. The test file is ignored by `tailor deploy` and never ships to the platform.
846
939
 
940
+ A migration generated under [`defaultDateRepresentation`](../configuration.md#application-settings) `"temporal"` or `"date"` records that in its `diff.json`, and deploy runs the script with Temporal or `Date` values for `t.date()`, `t.datetime()`, and `t.time()` fields that omit `as`. The scaffold calls `applyDateRepresentation` from `@tailor-platform/sdk/vitest` with the recorded value, or with `"string"` for a migration that records none, and imports `migrate.ts` after that call, so the test runs the script the same way deploy does even after the setting changes in `tailor.config.ts`. Keep that call when editing the test. The record pins the values at run time only: the types of `t` fields declared inside `migrate.ts` follow the current `tailor.d.ts` like everywhere else, so give such fields an explicit `as` to keep their types and values aligned after the setting changes. The helper selects the representation but does not provide `Temporal` itself, so run such a test in the [`tailor-runtime` Vitest environment](../testing.md#temporal), which installs it on Node.js versions without native Temporal; this is the one case where a migration unit test needs that environment (a migration recorded under `"date"`, or without a record, needs no Temporal and runs in a plain Vitest setup).
941
+
847
942
  ### Executing migrate.ts against a local Postgres (PGlite)
848
943
 
849
944
  A statement-level test verifies what the script issues, not what it does to data (e.g., whether a `where` clause matches the rows you intended). To run `main` against real rows locally, back Kysely with [`@electric-sql/pglite`](https://pglite.dev/) — an in-memory PostgreSQL — via `createKyselyPGlite` from `@tailor-platform/sdk/vitest`:
@@ -899,7 +994,7 @@ describe("0005 add required email", () => {
899
994
 
900
995
  Pass nested field values as JavaScript objects or arrays of objects, without `JSON.stringify`.
901
996
 
902
- When the migration was generated with `kyselyTypePlugin({ temporal: true })`, its `db.ts` types date and datetime fields as `Temporal.PlainDate` and `Temporal.Instant`, and top-level time fields as `Temporal.PlainTime`; nested time fields remain strings. Deploy runs the script with those values. Create the instance with `createKyselyPGlite<Unmigrated<Database>>(pglite, { temporal: true })` so PGlite reads top-level temporal columns back the same way; the scaffold adds the option for such migrations. Run the test in the [`tailor-runtime` Vitest environment](../testing.md#temporal), which provides `Temporal`. A migration generated before `temporal` was enabled still runs with `Date` values on deploy, so its test leaves the option out.
997
+ When the migration was generated with `kyselyTypePlugin({ temporal: true })`, its `db.ts` types date and datetime fields as `Temporal.PlainDate` and `Temporal.Instant`, and top-level time fields as `Temporal.PlainTime`; nested time fields remain strings. Deploy runs the script with those values. Create the instance with `createKyselyPGlite<Unmigrated<Database>>(pglite, { temporal: true })` so PGlite reads top-level temporal columns back the same way; the scaffold adds the option for such migrations. Run the test in the [`tailor-runtime` Vitest environment](../testing.md#temporal), which provides `Temporal`. A migration generated before `temporal` was enabled still runs with `Date` values on deploy, so its test leaves the option out. As in the Kysely mock scaffold, the test calls `applyDateRepresentation` with the migration's recorded `defaultDateRepresentation` (or `"string"`) so `t` date fields parse the way they do on deploy; a migration recorded under `"temporal"` likewise needs the `tailor-runtime` environment for `Temporal`.
903
998
  Generated migration types use `Record<string, unknown>` for each nested object so scripts can
904
999
  work with both old and new members during a migration; narrow member values before using them.
905
1000
 
@@ -908,6 +1003,33 @@ Two caveats keep this from replacing a scratch workspace:
908
1003
  - PGlite runs full PostgreSQL, while TailorDB supports [a subset of it](https://docs.tailor.tech/guides/function/accessing-tailordb#supported-sql-queries) — a statement that passes here can still be rejected on deploy.
909
1004
  - `db.pglite.ts` mirrors the column shape, not the platform: hooks, validations, and permissions do not run, and the limits listed under [Real SQL execution with PGlite](../testing.md#real-sql-execution-with-pglite-mocktailordbwithpglite) apply.
910
1005
 
1006
+ ### Testing a multi-step script
1007
+
1008
+ `runMigrationSteps` from `@tailor-platform/sdk/vitest` runs a script's `steps` in the order `tailor deploy` runs them, each through the transaction callback you pass, so a test can check what a failing step leaves behind. When a step throws, it rejects with a `MigrationStepError` naming the failed step and the steps that completed before it. With `pglite` and `Database` set up as in the PGlite test above:
1009
+
1010
+ ```typescript
1011
+ import {
1012
+ createKyselyPGlite,
1013
+ runMigrationSteps,
1014
+ type Unmigrated,
1015
+ } from "@tailor-platform/sdk/vitest";
1016
+ import { steps } from "./migrate";
1017
+
1018
+ const db = createKyselyPGlite<Unmigrated<Database>>(pglite);
1019
+
1020
+ test("backfills every table", async () => {
1021
+ await runMigrationSteps(steps, {
1022
+ transaction: (run) => db.transaction().execute(run),
1023
+ env: { STAGE: "test" },
1024
+ });
1025
+ // assert the rows
1026
+ });
1027
+ ```
1028
+
1029
+ `tailor tailordb migration script <N> --with-test` writes both test scaffolds against `steps` when `migrate.ts` exports `steps`, calling `runMigrationSteps` instead of `main`.
1030
+
1031
+ To test one step on its own, call its `run` with a transaction — and a `{ env }` context when the step takes one: `await db.transaction().execute((trx) => steps.backfillInvoice.run(trx))`. With `createKyselyMock`, pass `transaction: (run) => mock.withTx(run)`.
1032
+
911
1033
  ### Beyond unit tests
912
1034
 
913
1035
  A unit test verifies which statements the script issues; a PGlite test verifies what they do to the rows you staged. Neither runs against your actual data. To cover that:
@@ -932,7 +1054,7 @@ For genuinely different schemas across environments, prefer separate workspaces
932
1054
  1. `tailor tailordb migration status` to see local vs remote.
933
1055
  2. Compare with teammates — has someone applied different migrations?
934
1056
  3. If remote was changed manually, decide whether to update local migrations to match or to use `migration set <N>` to align bookkeeping.
935
- 4. To force the remote schema back to a known snapshot, use `migration sync <N>` (see [`migration sync` Semantics](#migration-sync-semantics)).
1057
+ 4. To force the remote schema back to a known snapshot, use `migration sync <N>` (see [`migration sync` Semantics](#migration-sync-semantics)). If the drift follows a failed deploy that warned it could not restore a table's schema, see [Failure Recovery](#failure-recovery) instead.
936
1058
  5. As a last resort in non-production environments, `--no-schema-check` skips both checks. Do not use this as a routine workaround.
937
1059
 
938
1060
  ### "Remote migration checkpoint is not in the local migration history" error
@@ -981,7 +1103,7 @@ One specific cause is a `migration rebaseline` run on a different environment wh
981
1103
 
982
1104
  **Cause:** Runtime error in your `migrate.ts`, a permission error from the machine user, or a constraint violation when post-migration tightens tables.
983
1105
 
984
- **Resolution:** Read the `Logs:` block. Fix the script or the data assumption it relies on, and re-run `tailor deploy`. The label is not bumped on failure, so the same migration retries.
1106
+ **Resolution:** Read the `Logs:` block. Fix the script or the data assumption it relies on, and re-run `tailor deploy`. The label is not bumped on failure, so the same migration retries. For a script that exports `steps`, the logs of each step are prefixed with its name, and the error names the failed step; the retry resumes from that step (see [Splitting a migration into steps](#splitting-a-migration-into-steps)).
985
1107
 
986
1108
  ### `migrate.ts` not found for a migration that needs one
987
1109
 
@@ -522,7 +522,7 @@ db.table("User", {
522
522
  - When not specified, `deploy` sets it from the executors taking part in the same run: `true` while one of them uses this table with `recordCreatedTrigger`, `recordUpdatedTrigger`, or `recordDeletedTrigger`, and `false` once none does. Removing the last such trigger turns publishing back off on the next `deploy`
523
523
  - When explicitly set to `false` while an executor taking part in the same run uses this table, `deploy` fails
524
524
  - An executor declared with `disabled: true` never runs, so it does not count as using the table
525
- - While a `deploy` applies pending migrations, every table in the migrating namespace is read-only: create, update, delete, and bulk-upsert operations are disabled while the existing read setting is preserved. Record event publishing is also switched off so a migration script's writes do not reach executors from the previous deploy. After success, the configured settings take effect. If a later migration fails, settings from the last confirmed checkpoint are restored; an uncommitted migration restores existing tables' prior settings and leaves its newly created tables restricted until a successful retry. Restoration is skipped if the checkpoint number or migration history changed concurrently, or if checkpoint ownership cannot be verified. A table left behind by a failed post-checkpoint deletion also remains read-only for manual recovery
525
+ - While a `deploy` applies pending migrations, every table in the migrating namespace is restricted: the GraphQL create, update, and delete operations and bulk upsert are disabled, and the table's `read` setting is not switched off for the migration (a table the migration creates uses its configured setting), so nothing writes an intermediate schema and readable tables are not shut down. Record event publishing is also switched off so a migration script's writes do not reach executors from the previous deploy. After success, the configured settings take effect. If a later migration fails, settings from the last confirmed checkpoint are restored; an uncommitted migration restores existing tables' prior settings and leaves its newly created tables restricted until a successful retry. A table that already refuses create, update, delete, and bulk upsert and publishes no record events is left as it is. Restoration is skipped if the checkpoint number or migration history changed concurrently, or if checkpoint ownership cannot be verified; the deploy then reports the namespace whose tables stay restricted and points to `tailor tailordb migration status --namespace <namespace>`. When a failed migration or a failed restoration leaves tables restricted, the deploy lists their names, and deploying again releases them once the migration completes. A table that a failed rollback could not delete or revert is the exception: the deploy also lists it, but the next deploy stops with remote schema drift until you delete or repair that table by hand. A table left behind by a failed post-checkpoint deletion also remains read-only for manual recovery, and the deploy does not list it by name
526
526
 
527
527
  **Use cases:**
528
528
 
@@ -567,7 +567,7 @@ db.table("User", {
567
567
 
568
568
  Control which GraphQL operations (`create`, `update`, `delete`, `read`) are exposed for a table. All operations are enabled by default.
569
569
 
570
- While a `deploy` applies pending migrations, every GraphQL operation — `create`, `update`, `delete`, `read`, and bulk upsert — is switched off across the migrating namespace, so nothing reads or writes an intermediate schema. After success, the configured operations take effect. If a later migration fails, operations from the last confirmed checkpoint are restored; an uncommitted migration restores existing tables' prior operations and leaves its newly created tables restricted until a successful retry. Restoration is skipped if the checkpoint number or migration history changed concurrently.
570
+ While a `deploy` applies pending migrations, the GraphQL `create`, `update`, and `delete` operations and bulk upsert are switched off across the migrating namespace, so nothing writes an intermediate schema. The `read` operation is not switched off: it keeps the setting the table has when the restrictions begin. A table that a migration touches then follows that migration's schema from its first phase on, so a migration that changes `read` changes it before its script runs; a table no migration touches keeps its setting. A table the migration creates uses its configured `read` setting from the start. After success, the configured operations take effect. If a later migration fails, operations from the last confirmed checkpoint are restored; an uncommitted migration restores existing tables' prior operations and leaves its newly created tables restricted until a successful retry. A table that already refuses create, update, delete, and bulk upsert and publishes no record events is left as it is. Restoration is skipped if the checkpoint number or migration history changed concurrently; the deploy then reports the namespace whose tables stay restricted, and deploying again releases them once the migration completes.
571
571
 
572
572
  ```typescript
573
573
  db.table("Order", {
package/docs/testing.md CHANGED
@@ -324,6 +324,7 @@ test("namespace-specific mock", async () => {
324
324
  disabled: false,
325
325
  mfaEnrolled: false,
326
326
  mfaFactorIds: [],
327
+ passwordUpdatedAt: null,
327
328
  });
328
329
 
329
330
  const client = new tailor.idp.Client({ namespace: "my-ns" });
@@ -446,6 +447,8 @@ export default defineConfig({
446
447
 
447
448
  This makes `tailor.secretmanager.getSecret("vault", "key")` return the values defined in your config. You can still override with `mockSecretmanager({ secrets: ... })` in individual tests: a per-test overlay applies only within that test, and the config-loaded secrets remain available to every other test.
448
449
 
450
+ The same `config` option applies the config's [`defaultDateRepresentation`](./configuration.md#application-settings) to tests: with `"temporal"`, `t.date()`, `t.datetime()`, and `t.time()` fields that omit `as` parse to Temporal values in test files and in the modules they import, as they do in deployed functions, and with `"date"` they parse to `Date` values. The config is read once when Vitest starts, in the Vitest process itself rather than under the `tailor-runtime` restrictions, so restart Vitest after changing the setting. A config file that is missing or does not parse fails the run, as does a value other than `"temporal"` or `"date"`. A config that throws while loading, for example on an environment variable only the deploy environment sets, prints a warning and keeps string values for that run, as the secrets loader already tolerates; the warning notes that tests then disagree with `tailor.d.ts` if the config sets `"temporal"` or `"date"`. Tests that call `resolver.body()` directly bypass the SDK's input conversion and must pass values in the representation the body expects. To apply a representation in a test file regardless of the config, for example in a plain Vitest setup or for a [migration test](./services/tailordb-migration.md#testing-migrations-locally) pinned to the value it was generated under, call `applyDateRepresentation` from `@tailor-platform/sdk/vitest` with `"temporal"`, `"date"`, or `"string"` (the representation of a project that leaves the setting unset); it returns a function that restores the previous representation. The helper only selects the representation: parsing such a field needs `Temporal`, which the `tailor-runtime` environment installs when Node.js lacks it, so outside that environment the test must provide a Temporal implementation itself.
451
+
449
452
  ### Per-Project Configuration
450
453
 
451
454
  Apply the runtime environment only to unit tests while keeping other test projects (e.g. e2e) in the default Node.js environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tailor-platform/sdk",
3
- "version": "2.26.0",
3
+ "version": "2.27.0",
4
4
  "description": "Tailor Platform SDK - The SDK to work with Tailor Platform",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -151,7 +151,7 @@
151
151
  "dependencies": {
152
152
  "@0no-co/graphql.web": "1.3.4",
153
153
  "@badgateway/oauth2-client": "3.3.1",
154
- "@bufbuild/protobuf": "2.15.0",
154
+ "@bufbuild/protobuf": "2.16.0",
155
155
  "@bufbuild/protovalidate": "1.3.0",
156
156
  "@connectrpc/connect": "2.2.0",
157
157
  "@connectrpc/connect-node": "2.2.0",
@@ -166,11 +166,11 @@
166
166
  "@opentelemetry/semantic-conventions": "1.43.0",
167
167
  "@oxc-project/types": "0.152.0",
168
168
  "@politty/zod": "0.3.0",
169
- "@secretlint/core": "13.0.6",
170
- "@secretlint/secretlint-rule-preset-recommend": "13.0.6",
169
+ "@secretlint/core": "13.0.7",
170
+ "@secretlint/secretlint-rule-preset-recommend": "13.0.7",
171
171
  "@standard-schema/spec": "1.1.0",
172
172
  "@tailor-platform/function-kysely-tailordb": "0.1.3",
173
- "@toiroakr/lines-db": "0.13.0",
173
+ "@toiroakr/lines-db": "0.15.0",
174
174
  "@toiroakr/read-multiline": "0.4.1",
175
175
  "@urql/core": "6.0.3",
176
176
  "amaro": "1.2.1",
@@ -180,13 +180,13 @@
180
180
  "find-up-simple": "1.0.1",
181
181
  "get-east-asian-width": "1.7.0",
182
182
  "get-tsconfig": "4.14.3",
183
- "globals": "17.12.0",
183
+ "globals": "17.13.0",
184
184
  "graphql": "17.0.2",
185
185
  "inflection": "3.0.2",
186
186
  "kysely": "0.29.6",
187
187
  "mime-types": "3.0.2",
188
188
  "open": "11.0.4",
189
- "oxc-parser": "0.151.0",
189
+ "oxc-parser": "0.152.0",
190
190
  "p-limit": "7.3.3",
191
191
  "pathe": "2.0.3",
192
192
  "pgsql-ast-parser": "12.0.2",
@@ -194,7 +194,7 @@
194
194
  "rolldown": "1.2.12",
195
195
  "semver": "7.8.5",
196
196
  "sql-highlight": "6.1.0",
197
- "std-env": "4.2.0",
197
+ "std-env": "4.3.0",
198
198
  "temporal-polyfill": "1.0.5",
199
199
  "temporal-spec": "1.0.1",
200
200
  "ts-cron-validator": "1.1.5",
@@ -208,18 +208,18 @@
208
208
  "@tailor-platform/shared": "^0.0.0",
209
209
  "@tailor-platform/tailor-proto": "^0.0.1",
210
210
  "@types/mime-types": "3.0.1",
211
- "@types/node": "24.19.0",
211
+ "@types/node": "24.19.1",
212
212
  "@types/semver": "7.8.0",
213
213
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
214
- "@vitest/coverage-v8": "5.0.1",
215
- "eslint-plugin-zod": "4.14.2",
214
+ "@vitest/coverage-v8": "5.0.3",
215
+ "eslint-plugin-zod": "5.0.0",
216
216
  "oxfmt": "0.71.0",
217
217
  "oxlint": "1.86.0",
218
218
  "oxlint-tsgolint": "7.0.2003",
219
219
  "sonda": "0.14.0",
220
220
  "tsdown": "0.23.0",
221
221
  "typescript": "6.0.3",
222
- "vitest": "5.0.1",
222
+ "vitest": "5.0.3",
223
223
  "zinfer": "0.4.7"
224
224
  },
225
225
  "peerDependencies": {
@@ -1 +0,0 @@
1
- import{n as e,t}from"./application-DS0XKBtK.mjs";export{t as defineApplication,e as generatePluginFilesIfNeeded};