@tailor-platform/sdk 2.28.0 → 2.29.1

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 (41) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +2 -9
  3. package/agent-skills/tailor/SKILL.md +2 -1
  4. package/dist/{application-CvmKh5WT.mjs → application-dalGHdrI.mjs} +1 -1
  5. package/dist/{application-CgJLyJaF.mjs → application-rWYDAfNU.mjs} +29 -29
  6. package/dist/application-rWYDAfNU.mjs.map +1 -0
  7. package/dist/cli/cache/bundle-cache.d.mts +1 -0
  8. package/dist/cli/lib.mjs +1 -1
  9. package/dist/cli/lib.mjs.map +1 -1
  10. package/dist/cli/main.d.mts +5 -0
  11. package/dist/cli/main.mjs +46 -46
  12. package/dist/cli/main.mjs.map +1 -1
  13. package/dist/cli/ts-hook.mjs +15 -11
  14. package/dist/completion/zsh-worker.zsh +59 -2
  15. package/dist/configure/config/types.d.mts +57 -0
  16. package/dist/configure/index.d.mts +1 -1
  17. package/dist/{register-ts-hook-yxshv0LV.mjs → register-ts-hook-CkfKqhJT.mjs} +78 -77
  18. package/dist/register-ts-hook-CkfKqhJT.mjs.map +1 -0
  19. package/dist/seed/index.d.mts +1 -1
  20. package/dist/seed/index.mjs +1 -1
  21. package/dist/seed/index.mjs.map +1 -1
  22. package/dist/seed-C3P_T_Eh.mjs.map +1 -1
  23. package/dist/types/helpers.d.mts +2 -5
  24. package/docs/cli/application.md +10 -5
  25. package/docs/cli/data.md +2 -0
  26. package/docs/cli/tailordb.md +36 -2
  27. package/docs/cli-reference.md +11 -0
  28. package/docs/configuration.md +55 -4
  29. package/docs/data-clone.md +71 -0
  30. package/docs/github-actions.md +90 -7
  31. package/docs/migration/v2.md +1 -1
  32. package/docs/plugin/custom.md +1 -1
  33. package/docs/quickstart.md +1 -1
  34. package/docs/services/resolver.md +1 -58
  35. package/docs/services/{tailordb.md → tailordb/index.md} +4 -4
  36. package/docs/services/{tailordb-migration.md → tailordb/migration.md} +24 -13
  37. package/docs/services/workflow.md +2 -2
  38. package/docs/testing.md +5 -5
  39. package/package.json +16 -16
  40. package/dist/application-CgJLyJaF.mjs.map +0 -1
  41. package/dist/register-ts-hook-yxshv0LV.mjs.map +0 -1
@@ -0,0 +1,71 @@
1
+ # Cloning Application Data
2
+
3
+ `tailor data clone` copies the data of an application from one workspace to another, for example to fill a staging workspace with production records before testing a release against them. It copies the records of every TailorDB namespace registered in the application, and optionally TailorDB file content and IdP users.
4
+
5
+ For the full option list, see [`data clone`](./cli/data.md#data-clone).
6
+
7
+ ## Prerequisites
8
+
9
+ The platform rejects a clone unless all of the following hold:
10
+
11
+ - The source and target workspaces are in the same region.
12
+ - You have editor access to both workspaces.
13
+ - The application is deployed to the target under the same name, with the same namespaces and the same TailorDB schema as the source. Deploy the version that runs in the source to the target first.
14
+ - The target is empty for everything the clone copies: the TailorDB records of the application's namespaces, plus its files with `--copy-files` and its IdP users with `--copy-idp-users`.
15
+
16
+ ## Cloning into another workspace
17
+
18
+ 1. Deploy the source's version of the application to the target workspace:
19
+
20
+ ```bash
21
+ tailor deploy -w <target-workspace-id>
22
+ ```
23
+
24
+ 2. If the target already has records, empty its TailorDB tables:
25
+
26
+ ```bash
27
+ tailor tailordb truncate --all -w <target-workspace-id>
28
+ ```
29
+
30
+ `truncate` removes TailorDB records only. When you clone files or IdP users, remove those from the target separately.
31
+
32
+ 3. Start the clone:
33
+
34
+ ```bash
35
+ tailor data clone --from <source-workspace-id> --to <target-workspace-id>
36
+ ```
37
+
38
+ The application is the one named by `name` in `tailor.config.ts`, so run all three steps from that application's project. The command asks for confirmation unless `--yes` is given.
39
+
40
+ ## What is copied
41
+
42
+ | Data | Copied |
43
+ | ------------------------- | ------------------------- |
44
+ | TailorDB records | Always |
45
+ | TailorDB file content | With `--copy-files` |
46
+ | IdP users, with passwords | With `--copy-idp-users` |
47
+ | Configuration and schema | No — deploy them yourself |
48
+
49
+ Without `--copy-files`, file fields in the target keep their references, but the content they point to is absent. Without `--copy-idp-users`, records that store IdP user IDs refer to users that do not exist in the target.
50
+
51
+ Cloning IdP users copies credentials that sign in to the source. Do not clone users from production into a workspace that people outside the production audience can reach.
52
+
53
+ ## Waiting for a clone
54
+
55
+ A clone runs asynchronously on the platform. The command prints an operation ID when the clone starts, then waits until it completes or fails.
56
+
57
+ Pressing Ctrl+C, or passing `--no-wait`, stops only the waiting; the clone keeps running. Check on it later with the operation ID:
58
+
59
+ ```bash
60
+ # Print the current status once
61
+ tailor data operation get --from <source-workspace-id> --to <target-workspace-id> --operation-id <operation-id>
62
+
63
+ # Wait until it completes or fails
64
+ tailor data operation get --from <source-workspace-id> --to <target-workspace-id> --operation-id <operation-id> --wait
65
+ ```
66
+
67
+ With `--json`, `data clone` reports `status: "completed"` once the clone has finished, or `status: "started"` with `--no-wait`.
68
+
69
+ ## Rehearsing migrations with cloned data
70
+
71
+ To test pending TailorDB migrations against a copy of real records, use `tailor tailordb migration test --data clone` instead of cloning by hand. It creates a temporary workspace, deploys the pre-migration schema, clones the records, and runs the pending migrations. That command copies TailorDB records only; see [Clone mode](./services/tailordb/migration.md#clone-mode).
@@ -35,7 +35,8 @@ tailor setup ci tag --name my-app-prod \
35
35
 
36
36
  After running the command, follow the **Next steps** printed to the terminal:
37
37
  run `tailor setup ci env` to get the commands that set the secrets and
38
- variables each GitHub Environment needs (see
38
+ variables each GitHub Environment needs, and `tailor setup ci env --check` to
39
+ confirm none is missing (see
39
40
  [Setting secrets and variables](#setting-secrets-and-variables)), then commit
40
41
  the generated files.
41
42
 
@@ -98,7 +99,8 @@ The generated workflow builds one self-contained ERD viewer HTML file for each
98
99
  owned TailorDB namespace in `tailor.config.ts`. The viewer compares the pull
99
100
  request merge result with the base branch, can switch between the current schema
100
101
  and highlighted diff, uploads the HTML files as unarchived Actions artifacts,
101
- and upserts a PR comment with artifact links.
102
+ and upserts a PR comment with artifact links. On GitHub Enterprise Server, also
103
+ pass `--ghes` (see [GitHub Enterprise Server](#github-enterprise-server)).
102
104
 
103
105
  ERD preview does not use Tailor Platform credentials. Fork pull requests still
104
106
  build artifacts, but the comment step is skipped because fork tokens cannot
@@ -264,8 +266,8 @@ Everything else is yours, and re-running `setup` keeps it:
264
266
  - **These inputs of managed steps:** `ignore` on `tailor-generate-check` and
265
267
  `tailor-drift-check`, `fail-on-drift` on `tailor-drift-check`,
266
268
  `install-command` on `tailor-install`, `node-version-file` on
267
- `tailor-setup`, `label` on `tailor-plan`, and `user-mapping` on
268
- `tailor-notify`.
269
+ `tailor-setup`, `label` on `tailor-plan`, `ttl` on `tailor-preview-deploy`,
270
+ and `user-mapping` on `tailor-notify`.
269
271
  - **Your own jobs in the `needs` of `tailor-result` and
270
272
  `tailor-preview-result`**, so they count toward the workflow's result (see
271
273
  [Requiring the workflow in branch protection](#requiring-the-workflow-in-branch-protection)).
@@ -296,6 +298,17 @@ The job is skipped whenever `tailor-preview-deploy` is skipped: when a pull
296
298
  request is closed, for draft and fork pull requests, and for unlabeled ones with
297
299
  `--require-preview-label`.
298
300
 
301
+ A preview workspace expires 7 days (`ttl: 7d` on `tailor-preview-deploy`) after
302
+ the last push to its pull request. When a pull request is closed,
303
+ `tailor-preview-cleanup` deletes its workspace and also deletes the other
304
+ expired preview workspaces of the same workspace name in the folder
305
+ (`TAILOR_PLATFORM_FOLDER_ID`, or directly under the organization when unset), so
306
+ a workspace whose own cleanup never ran does not stay forever. A pull request
307
+ that stays idle past the `ttl` can have its workspace deleted while it is still
308
+ open; the next push creates a fresh one. Change `ttl` to keep previews longer or
309
+ shorter. Workflows generated before this was added get these settings the next
310
+ time you run `tailor setup update`.
311
+
299
312
  Each new preview workflow run for a pull request, such as after a push or when
300
313
  the pull request is closed, cancels the run still in progress for it, including
301
314
  your jobs, so they do not keep running against a workspace that the new run
@@ -468,14 +481,18 @@ protection rules no longer guard your deploys.
468
481
  Environment the generated workflows use, the commands that create the
469
482
  environment (only when it does not exist yet) and set its secrets and
470
483
  variables. It is read-only, so re-run it whenever you add a target. When the
471
- `origin` remote points at github.com, the output names that repository, so the
472
- commands work from any directory; otherwise `gh` resolves the repository from
473
- the current directory and the Terraform output leaves it as a placeholder.
484
+ `origin` remote points at github.com, the output names that repository;
485
+ otherwise it names the repository `gh repo view` reports for the current
486
+ directory, with its host on GitHub Enterprise Server. Either way the commands
487
+ work from any directory. When neither names a repository, `gh` resolves it from
488
+ the current directory when you run the commands, and the Terraform output leaves
489
+ it as a placeholder.
474
490
 
475
491
  ```bash
476
492
  tailor setup ci env # gh CLI commands (default)
477
493
  tailor setup ci env --format terraform # Terraform for the integrations/github provider
478
494
  tailor setup ci env --environment my-app-stg # only one environment (repeat for several)
495
+ tailor setup ci env --check # check what is registered in GitHub (see below)
479
496
  ```
480
497
 
481
498
  The list follows what each generated workflow actually reads:
@@ -517,6 +534,30 @@ protection settings (reviewers, wait timer, branch policy), so importing an
517
534
  environment keeps the approval gate you configured; remove the `lifecycle` block
518
535
  to manage those settings in Terraform instead.
519
536
 
537
+ ### Checking what is registered
538
+
539
+ `tailor setup ci env --check` reads GitHub with `gh` and reports, for each
540
+ environment, which of these entries are registered. It fails when a required
541
+ entry is missing, reports missing optional entries without failing, and prints
542
+ the `gh` commands that set only the missing entries, with where each value comes
543
+ from. It changes nothing, and it checks names only, since GitHub never returns
544
+ secret values.
545
+
546
+ - An entry set on the repository, or on the organization and shared with the
547
+ repository, counts as registered, because the workflow reads it from there when
548
+ the environment does not set it. The report says where each entry was found.
549
+ - An entry registered as the other kind, such as `TAILOR_PLATFORM_WORKSPACE_ID`
550
+ set as a secret, is reported as missing, because the workflow does not read it.
551
+ `TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID` set as a secret still counts, with a
552
+ reminder to move it to a variable.
553
+ - `gh` must be installed and logged in (`gh auth login`) as an account with write
554
+ access to the repository, which listing secrets and variables requires. GitHub
555
+ answers 404 for a private repository the account cannot see, so the check
556
+ reports that as an access problem, not as missing entries.
557
+
558
+ `--environment` narrows the check the same way. `--check` cannot be combined with
559
+ `--format terraform`.
560
+
520
561
  ## GitHub Environments (approval gate)
521
562
 
522
563
  Both the `plan` and `deploy` jobs are associated with a GitHub Environment — the
@@ -784,6 +825,48 @@ tailor setup ci env
784
825
 
785
826
  Commit both workflow files and `.github/tailor.lock`.
786
827
 
828
+ ## GitHub Enterprise Server
829
+
830
+ The generated workflows also run on GitHub Enterprise Server. Prepare the
831
+ server and its runners first:
832
+
833
+ - **Runners:** GitHub Enterprise Server has no GitHub-hosted runners. Change
834
+ `runs-on` on the managed jobs to the labels of your self-hosted runners;
835
+ re-running `setup` keeps it (see
836
+ [Customizing the generated workflow](#customizing-the-generated-workflow)).
837
+ The runners need Actions runner 2.327.1 or later, which runs the Node 24
838
+ actions the workflows use, and `gh` and `jq` on `PATH` (plus `node` with
839
+ `--erd-preview`).
840
+ - **Actions from github.com:** the workflows use `tailor-platform/actions`,
841
+ `actions/checkout`, `actions/setup-node`, `pnpm/action-setup`, and
842
+ `oven-sh/setup-bun` (and `actions/upload-artifact` with `--erd-preview`).
843
+ Make them available on the server with GitHub Connect or `actions-sync`. The
844
+ server ships its own copies of the `actions/*` ones, which may not include
845
+ the pinned versions: update them with `actions-sync`, or, with GitHub
846
+ Connect, delete them from the server's `actions` organization (see GitHub's
847
+ [Using the latest version of the official bundled actions](https://docs.github.com/en/enterprise-server@latest/admin/managing-github-actions-for-your-enterprise/managing-access-to-actions-from-githubcom/using-the-latest-version-of-the-official-bundled-actions)).
848
+ - **Environment settings:** sign `gh` in to the server
849
+ (`gh auth login --hostname <host>`) before you run `tailor setup ci env` in a
850
+ clone of the repository, so that the `gh` commands it prints and its Terraform
851
+ output target the server. When `gh` cannot tell the repository, the output
852
+ says to export `GH_HOST=<host>` before running the `gh` commands, or
853
+ `GITHUB_BASE_URL=https://<host>/` before running Terraform, so that they
854
+ call the server instead of github.com.
855
+
856
+ With `--erd-preview`, also pass `--ghes`:
857
+
858
+ ```bash
859
+ tailor setup ci branch --name my-app-stg --erd-preview --ghes
860
+ ```
861
+
862
+ GitHub Enterprise Server does not support `actions/upload-artifact` v4 or
863
+ later, which the workflow uses to upload each ERD viewer unzipped. With
864
+ `--ghes`, the workflow uploads it with v3 instead, so each viewer is a zip
865
+ file, and the PR comment links to the workflow run, which lists the artifacts.
866
+ `setup update` keeps `--ghes`; pass it again whenever you re-run
867
+ `setup ci branch`, or the workflow switches back to the upload that GitHub
868
+ Enterprise Server does not support.
869
+
787
870
  ## Keeping dependencies and actions updated
788
871
 
789
872
  Run this command once from the repository root to add Renovate configuration:
@@ -1736,7 +1736,7 @@ The CLI stores human users by their stable subject ID instead of email (email is
1736
1736
 
1737
1737
  ### `db.fields.timestamps()`: `updatedAt` becomes required
1738
1738
 
1739
- The `updatedAt` field from `db.fields.timestamps()` changes from optional to required (non-null): it defaults to the current time and keeps refreshing automatically on every update, though a value you provide explicitly is still respected. Applying this change to a table that already has rows with `updatedAt: null` makes `deploy` fail with `field "updatedAt" cannot be updated from non-required to required when records with null values exist`. Backfill those rows first, e.g. `UPDATE <table> SET "updatedAt" = "createdAt" WHERE "updatedAt" IS NULL` for each affected table — see [TailorDB migrations](../services/tailordb-migration.md#performance-and-large-tables) for splitting a large backfill across primary-key ranges if a single `UPDATE` times out.
1739
+ The `updatedAt` field from `db.fields.timestamps()` changes from optional to required (non-null): it defaults to the current time and keeps refreshing automatically on every update, though a value you provide explicitly is still respected. Applying this change to a table that already has rows with `updatedAt: null` makes `deploy` fail with `field "updatedAt" cannot be updated from non-required to required when records with null values exist`. Backfill those rows first, e.g. `UPDATE <table> SET "updatedAt" = "createdAt" WHERE "updatedAt" IS NULL` for each affected table — see [TailorDB migrations](../services/tailordb/migration.md#performance-and-large-tables) for splitting a large backfill across primary-key ranges if a single `UPDATE` times out.
1740
1740
 
1741
1741
  ### Node.js minimum version raised to 22.15.0
1742
1742
 
@@ -516,9 +516,9 @@ export default createSoftDeletePlugin();
516
516
  ```typescript
517
517
  // plugins/soft-delete/executors/on-delete.ts
518
518
  import { createExecutor, recordDeletedTrigger } from "@tailor-platform/sdk";
519
- import type { TailorAnyDBType } from "@tailor-platform/sdk";
520
519
  import { withPluginContext } from "@tailor-platform/sdk/plugin";
521
520
  import { getDB } from "generated/tailordb";
521
+ import type { TailorAnyDBType } from "@tailor-platform/sdk";
522
522
 
523
523
  interface SoftDeleteContext {
524
524
  sourceTable: TailorAnyDBType;
@@ -108,7 +108,7 @@ Deploy again to see the response.
108
108
 
109
109
  ## Next Steps
110
110
 
111
- - Learn about [TailorDB](./services/tailordb.md) for database schema definition
111
+ - Learn about [TailorDB](./services/tailordb/index.md) for database schema definition
112
112
  - Create custom [Resolvers](./services/resolver.md) with business logic
113
113
  - Set up [Executors](./services/executor.md) for event-driven automation
114
114
  - Explore [Templates](https://github.com/tailor-platform/sdk/tree/main/packages/create-sdk#available-templates) for more examples
@@ -11,64 +11,7 @@ Resolvers provide:
11
11
  - Access to TailorDB via Kysely query builder
12
12
  - User context for authentication/authorization
13
13
 
14
- ## Comparison with Tailor Platform Pipeline Resolver
15
-
16
- The SDK's Resolver is a simplified version of Tailor Platform's [Pipeline Resolver](https://docs.tailor.tech/guides/resolver).
17
-
18
- | Pipeline Resolver | SDK Resolver |
19
- | ---------------------------------------- | --------------------------------- |
20
- | Multiple steps with different operations | Single `body` function |
21
- | Declarative step configuration | Imperative TypeScript code |
22
- | Built-in TailorDB/GraphQL steps | Direct database access via Kysely |
23
- | CEL expressions for data transformation | Native TypeScript transformations |
24
-
25
- ### Example Comparison
26
-
27
- **Pipeline Resolver (Tailor Platform native):**
28
-
29
- ```yaml
30
- steps:
31
- - name: getUser
32
- operation: tailordb.query
33
- params:
34
- type: User
35
- filter:
36
- email: { eq: "{{ input.email }}" }
37
- - name: updateAge
38
- operation: tailordb.mutation
39
- params:
40
- type: User
41
- id: "{{ steps.getUser.id }}"
42
- input:
43
- age: "{{ steps.getUser.age + 1 }}"
44
- ```
45
-
46
- **Resolver (SDK):**
47
-
48
- ```typescript
49
- createResolver({
50
- name: "incrementUserAge",
51
- operation: "mutation",
52
- input: { email: t.string() },
53
- body: async (context) => {
54
- const db = getDB("tailordb");
55
- const user = await db
56
- .selectFrom("User")
57
- .selectAll()
58
- .where("email", "=", context.input.email)
59
- .executeTakeFirstOrThrow();
60
-
61
- await db
62
- .updateTable("User")
63
- .set({ age: user.age + 1 })
64
- .where("id", "=", user.id)
65
- .execute();
66
-
67
- return { oldAge: user.age, newAge: user.age + 1 };
68
- },
69
- output: t.object({ oldAge: t.int(), newAge: t.int() }),
70
- });
71
- ```
14
+ For the official Tailor Platform documentation, see [Resolver Guide](https://docs.tailor.tech/guides/resolver).
72
15
 
73
16
  ## Creating a Resolver
74
17
 
@@ -535,7 +535,7 @@ db.table("User", {
535
535
  - 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`
536
536
  - When explicitly set to `false` while an executor taking part in the same run uses this table, `deploy` fails
537
537
  - An executor declared with `disabled: true` never runs, so it does not count as using the table
538
- - While a `deploy` applies pending migrations with [maintenance mode](./tailordb-migration.md#maintenance-mode) on, 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. With `maintenanceMode: false`, these restrictions are not applied. 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
538
+ - While a `deploy` applies pending migrations with [maintenance mode](./migration.md#maintenance-mode) on, 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. With `maintenanceMode: false`, these restrictions are not applied. 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
539
539
 
540
540
  **Use cases:**
541
541
 
@@ -580,7 +580,7 @@ db.table("User", {
580
580
 
581
581
  Control which GraphQL operations (`create`, `update`, `delete`, `read`) are exposed for a table. All operations are enabled by default.
582
582
 
583
- While a `deploy` applies pending migrations with [maintenance mode](./tailordb-migration.md#maintenance-mode) on, 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. With or without maintenance mode, a table that a migration touches 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 while maintenance mode is on, 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.
583
+ While a `deploy` applies pending migrations with [maintenance mode](./migration.md#maintenance-mode) on, 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. With or without maintenance mode, a table that a migration touches 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 while maintenance mode is on, 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.
584
584
 
585
585
  ```typescript
586
586
  db.table("Order", {
@@ -744,6 +744,6 @@ db.table("User", {
744
744
 
745
745
  ## Migrations
746
746
 
747
- When you change a TailorDB table definition, the SDK can generate a migration that captures the diff and, for breaking changes, runs a data transformation script during `tailor deploy`. See the [TailorDB Migrations guide](./tailordb-migration.md) for the full workflow, configuration, supported change types, team coordination, and troubleshooting.
747
+ When you change a TailorDB table definition, the SDK can generate a migration that captures the diff and, for breaking changes, runs a data transformation script during `tailor deploy`. See the [TailorDB Migrations guide](./migration.md) for the full workflow, configuration, supported change types, team coordination, and troubleshooting.
748
748
 
749
- For the CLI command reference, see [`tailordb migration`](../cli/tailordb.md#tailordb-migration).
749
+ For the CLI command reference, see [`tailordb migration`](../../cli/tailordb.md#tailordb-migration).
@@ -4,7 +4,7 @@
4
4
 
5
5
  The migration system tracks changes to your TailorDB table definitions over time and applies them to deployed workspaces with optional data transformation scripts.
6
6
 
7
- For the CLI command reference, see [`tailordb migration`](../cli/tailordb.md#tailordb-migration). This document covers concepts, workflows, and operational guidance.
7
+ For the CLI command reference, see [`tailordb migration`](../../cli/tailordb.md#tailordb-migration). This document covers concepts, workflows, and operational guidance.
8
8
 
9
9
  ## Overview
10
10
 
@@ -244,7 +244,7 @@ This writes a numbered migration with an empty `diff.json`, a `migrate.ts` skele
244
244
 
245
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`.
246
246
 
247
- A data-only migration runs in **every** workspace the history is applied to, including freshly created ones. Write the script so it is safe against tables with no matching rows (a set-based `UPDATE` with a `WHERE` clause is naturally a no-op on an empty table). For a fix that should run in a single environment only, or that is too large for one transaction, run it outside the migration history instead, for example as a one-off script scaffolded with [`tailor function script`](../cli/function.md#function-script) and executed against a single workspace with [`tailor function run`](../cli/function.md#function-run).
247
+ A data-only migration runs in **every** workspace the history is applied to, including freshly created ones. Write the script so it is safe against tables with no matching rows (a set-based `UPDATE` with a `WHERE` clause is naturally a no-op on an empty table). For a fix that should run in a single environment only, or that is too large for one transaction, run it outside the migration history instead, for example as a one-off script scaffolded with [`tailor function script`](../../cli/function.md#function-script) and executed against a single workspace with [`tailor function run`](../../cli/function.md#function-run).
248
248
 
249
249
  ## Configuration
250
250
 
@@ -425,7 +425,7 @@ Because steps commit separately, a failure does not undo the steps that already
425
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
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
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.
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, or settle it with `migration resolve` (see [Settling a migration that stays in progress](#settling-a-migration-that-stays-in-progress)). 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
429
 
430
430
  #### Editing steps of a migration in progress
431
431
 
@@ -450,6 +450,15 @@ tailor deploy --migration-skip-steps main-db/backfillUser,main-db/backfillInvoic
450
450
 
451
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. Rows written while a migration is in progress are not seen by steps that already completed. [Maintenance mode](#maintenance-mode) blocks GraphQL writes but not the writes your resolvers, executors, or workflows make through Kysely, and without it GraphQL clients can write too, so stop such writers until the migration finishes or write steps that tolerate them.
452
452
 
453
+ ### Settling a migration that stays in progress
454
+
455
+ When you do not want to resume the migration with another deploy — you abandoned it, or you repaired the data by hand — settle it with `tailor tailordb migration resolve`. Pass exactly one of two options:
456
+
457
+ - `--rollback` sends the schema of the migration before the failed one, clears the in-progress record, and leaves the checkpoint where it was. The next `tailor deploy` runs the failed migration again from its first step, so every step must be safe to run again. Data changes made by steps that already completed are not undone, and tables that exist only in the failed migration's schema are deleted with their data.
458
+ - `--complete` sends the schema of the failed migration itself, clears the in-progress record, and moves the checkpoint to that migration, without running its `migrate.ts` again. Repair the data first, for example with `tailor query`, and make sure no record lacks a value for a field the migration makes required: the update is rejected while such a record exists, and the migration stays in progress.
459
+
460
+ Tables that the failed migration removes are deleted with their data under `--complete`. The command refuses to run while an execution of the failed migration is still running; wait for it to finish first. Both options take the namespace's tables out of [maintenance mode](#maintenance-mode) and remove the temporary resources of the failed run. The command shows what it will change and asks for confirmation (`--yes` skips it). As with `migration sync`, the local migration history must reproduce the current table definitions, so settle the migration before you change the tables. If sending the schema stops partway, run the same command again.
461
+
453
462
  ## Supported Schema Changes
454
463
 
455
464
  | Change Type | Breaking? | Migration Script? | Notes |
@@ -629,7 +638,9 @@ Clone mode copies TailorDB records from the source workspace after the identical
629
638
  tailor tailordb migration test --data clone
630
639
  ```
631
640
 
632
- The platform clone API is feature-gated and requires editor access to both same-region workspaces. It copies TailorDB records only: IdP users, file blobs, and metadata labels are not copied. File fields therefore retain references whose blobs are absent. The command polls the asynchronous operation and reports platform failures; if clone is unavailable, use seed mode.
641
+ Cloning requires editor access to both workspaces, which must be in the same region. Clone mode copies TailorDB records only: IdP users, file blobs, and metadata labels are not copied, and unlike [`tailor data clone`](../../data-clone.md) it has no `--copy-files` or `--copy-idp-users` option. File fields therefore retain references whose blobs are absent. The command polls the asynchronous operation and reports platform failures.
642
+
643
+ With `--target-workspace-id`, the designated target must be in the same region as the source; the command checks this before deploying anything to it.
633
644
 
634
645
  The source schema is re-verified immediately before cloning; if the source workspace was deployed or otherwise changed after the test started, the command aborts instead of cloning data that no longer matches the deployed baseline.
635
646
 
@@ -687,7 +698,7 @@ This split is what allows existing rows to be backfilled before the database sta
687
698
  | `"migration"` (default) | From before the first pending migration runs until the last one completes. |
688
699
  | `"deploy"` | From before the first pending migration runs until the deploy has applied every other change, including resolvers, executors, and workflows, and has removed the resources it deletes. |
689
700
 
690
- A deploy without pending migrations never enters maintenance mode. When `maintenanceMode` is unset, it behaves as `"migration"`, preserving the existing deployment behavior. The value is read from the config like any other, so it can differ per environment (see [Multi-Environment Configuration](../multi-environment.md#varying-config-values-per-environment)):
701
+ A deploy without pending migrations never enters maintenance mode. When `maintenanceMode` is unset, it behaves as `"migration"`, preserving the existing deployment behavior. The value is read from the config like any other, so it can differ per environment (see [Multi-Environment Configuration](../../multi-environment.md#varying-config-values-per-environment)):
691
702
 
692
703
  ```typescript
693
704
  export default defineConfig({
@@ -711,7 +722,7 @@ Maintenance mode restricts GraphQL writes and record events only. Resolvers, exe
711
722
 
712
723
  #### Keeping maintenance mode short
713
724
 
714
- Maintenance mode lasts for the whole migration job, including the time the platform takes to start it, which can far exceed the time `migrate.ts` itself needs. Keep in `migrate.ts` only the work that must be done before the post-migration schema is applied, and move the rest into a workflow that you start after the deploy (for example with [`tailor workflow start`](../cli/workflow.md#workflow-start)).
725
+ Maintenance mode lasts for the whole migration job, including the time the platform takes to start it, which can far exceed the time `migrate.ts` itself needs. Keep in `migrate.ts` only the work that must be done before the post-migration schema is applied, and move the rest into a workflow that you start after the deploy (for example with [`tailor workflow start`](../../cli/workflow.md#workflow-start)).
715
726
 
716
727
  - **Must stay in `migrate.ts`**: anything the post-migration schema enforces or removes — filling a field that becomes required, resolving duplicates before a unique index, converting values for a field type change, and copying data out of a field or table that the migration renames or removes.
717
728
  - **Can move to a workflow**: work the post-migration schema does not depend on, such as filling an optional field, recomputing derived values, or cleaning up rows. Requests that run before the workflow finishes see the data in its unfixed state.
@@ -734,7 +745,7 @@ When [maintenance mode](#maintenance-mode) is on, `tailor deploy` reports how lo
734
745
 
735
746
  Phases that repeat across several migrations are added up. The report is printed only after the migrations succeed and the tables leave maintenance mode; when a migration fails, deploy reports the error instead (see [Failure Recovery](#failure-recovery)). When deploy resumes a migration that an earlier deploy left in progress, the tables were already in maintenance mode before it started, and the report covers only this deploy. A job can be reported as started on the platform well before its code runs, so deploy tells waiting from running by a line every job of a migration logs before your script: `[tailor-sdk] migration script started`. You may see that line in function execution logs; deploy leaves it out of the logs it prints. A script that exports [`steps`](#splitting-a-migration-into-steps) waits for each step's job to start, and that time counts as `waiting to start` too. Time when deploy cannot tell whether the script is running — because the run or a running job's logs could not be read, or because an older SDK version started the run and it never logs the line — is reported as `waiting to start or running` instead.
736
747
 
737
- Deploy checks the run every few seconds, so a boundary between `waiting to start` and `running` can be off by that much. With `--json`, the same breakdown is in `tailordbMaintenance` (see [Deploy JSON result](../cli/application.md#deploy)). Add `--verbose` to also print how long each phase took and every change in the job's status.
748
+ Deploy checks the run every few seconds, so a boundary between `waiting to start` and `running` can be off by that much. With `--json`, the same breakdown is in `tailordbMaintenance` (see [Deploy JSON result](../../cli/application.md#deploy)). Add `--verbose` to also print how long each phase took and every change in the job's status.
738
749
 
739
750
  ### Schema verification
740
751
 
@@ -976,8 +987,8 @@ When `migrate.ts` already exists (the usual case for breaking changes, where `mi
976
987
  // migrations/0005/migrate.test.ts
977
988
  import { createKyselyMock } from "@tailor-platform/sdk/vitest";
978
989
  import { describe, expect, test } from "vitest";
979
- import type { Database } from "./db";
980
990
  import { main } from "./migrate";
991
+ import type { Database } from "./db";
981
992
 
982
993
  describe("0005 add required email", () => {
983
994
  test("backfills null emails", async () => {
@@ -992,11 +1003,11 @@ describe("0005 add required email", () => {
992
1003
  });
993
1004
  ```
994
1005
 
995
- Stage the rows each query returns with `mock.enqueueResult(...)` or `mock.setQueryResolver(...)` when the script reads before writing; call `main(trx, { env: { ... } })` when the script takes a `MigrationContext`. See [Kysely-layer mock](../testing.md#kysely-layer-mock-createkyselymock) for the full mock API.
1006
+ Stage the rows each query returns with `mock.enqueueResult(...)` or `mock.setQueryResolver(...)` when the script reads before writing; call `main(trx, { env: { ... } })` when the script takes a `MigrationContext`. See [Kysely-layer mock](../../testing.md#kysely-layer-mock-createkyselymock) for the full mock API.
996
1007
 
997
1008
  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.
998
1009
 
999
- 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).
1010
+ 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).
1000
1011
 
1001
1012
  ### Executing migrate.ts against a local Postgres (PGlite)
1002
1013
 
@@ -1013,9 +1024,9 @@ The generated `db.pglite.ts` exports the `CREATE TABLE` script for the same sche
1013
1024
  import { PGlite } from "@electric-sql/pglite";
1014
1025
  import { createKyselyPGlite, type Unmigrated } from "@tailor-platform/sdk/vitest";
1015
1026
  import { afterAll, beforeAll, describe, expect, test } from "vitest";
1016
- import type { Database } from "./db";
1017
1027
  import { pgliteSchema } from "./db.pglite";
1018
1028
  import { main } from "./migrate";
1029
+ import type { Database } from "./db";
1019
1030
 
1020
1031
  const pglite = new PGlite();
1021
1032
  const db = createKyselyPGlite<Unmigrated<Database>>(pglite);
@@ -1053,14 +1064,14 @@ describe("0005 add required email", () => {
1053
1064
 
1054
1065
  Pass nested field values as JavaScript objects or arrays of objects, without `JSON.stringify`.
1055
1066
 
1056
- 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`.
1067
+ 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`.
1057
1068
  Generated migration types use `Record<string, unknown>` for each nested object so scripts can
1058
1069
  work with both old and new members during a migration; narrow member values before using them.
1059
1070
 
1060
1071
  Two caveats keep this from replacing a scratch workspace:
1061
1072
 
1062
1073
  - 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.
1063
- - `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.
1074
+ - `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.
1064
1075
 
1065
1076
  ### Testing a multi-step script
1066
1077
 
@@ -488,9 +488,9 @@ Pass the runtime key through the `executionPolicyKey` option on `job.start()`. F
488
488
 
489
489
  ```typescript
490
490
  import { createWorkflowJob } from "@tailor-platform/sdk";
491
- import { executionPolicies } from "./policies";
492
- import { sendNotification } from "./jobs/send-notification";
493
491
  import { fetchTenant } from "./jobs/fetch-tenant";
492
+ import { sendNotification } from "./jobs/send-notification";
493
+ import { executionPolicies } from "./policies";
494
494
 
495
495
  export const mainJob = createWorkflowJob({
496
496
  name: "main-job",
package/docs/testing.md CHANGED
@@ -44,9 +44,9 @@ The Tailor Platform function runtime only provides Web Standard APIs. Node.js bu
44
44
  ### Setup
45
45
 
46
46
  ```typescript
47
+ import { tailorRuntime } from "@tailor-platform/sdk/vitest";
47
48
  // vitest.config.ts
48
49
  import { defineConfig } from "vitest/config";
49
- import { tailorRuntime } from "@tailor-platform/sdk/vitest";
50
50
 
51
51
  export default defineConfig({
52
52
  plugins: [tailorRuntime()],
@@ -219,7 +219,7 @@ To hand-write DDL instead — for a table not in the schema, or to add a constra
219
219
 
220
220
  - The PGlite instance is yours: the mock never closes it, so close it in `afterAll`. Reuse one instance across a suite — creating one per test is slow.
221
221
  - Pass the same instance under several namespaces to drive them against one shared database. Two namespaces with a same-named table cannot share one instance, because the second script leaves the first table as it is.
222
- - Seed through `getDB` itself. When a column type rejects a value that only the test must stage, use `createKyselyPGlite<Unmigrated<...>>(pglite)` instead — see [Testing Migrations Locally](./services/tailordb-migration.md#testing-migrations-locally). This only affects test setup; it cannot supply a `.serial()` value for an insert issued by the code under test.
222
+ - Seed through `getDB` itself. When a column type rejects a value that only the test must stage, use `createKyselyPGlite<Unmigrated<...>>(pglite)` instead — see [Testing Migrations Locally](./services/tailordb/migration.md#testing-migrations-locally). This only affects test setup; it cannot supply a `.serial()` value for an insert issued by the code under test.
223
223
  - With `kyselyTypePlugin({ temporal: true })`, the generated `getDB` reads top-level date, datetime, and time columns back from PGlite as `Temporal.PlainDate`, `Temporal.Instant`, and `Temporal.PlainTime`, as it does on the platform. Values inside nested objects stay strings in PGlite (see above); on the platform, nested date/datetime fields are Temporal values while nested time fields remain strings.
224
224
  - Transactions on a shared instance are serialized: while one is open, queries from other `getDB` instances wait. Do not use `test.concurrent` with a shared instance, and do not query the same instance through a second `getDB` from inside a transaction — that waits on itself.
225
225
  - PGlite runs full PostgreSQL while TailorDB supports a subset of it, and TailorDB hooks, validations, and permissions do not run here — a test passing on PGlite can still behave differently on the platform. Keep [`mockTailordb`](#tailordb-mock) or [`createKyselyMock`](#kysely-layer-mock-createkyselymock) tests for query shape and error paths, and E2E tests for platform behavior.
@@ -448,7 +448,7 @@ export default defineConfig({
448
448
 
449
449
  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.
450
450
 
451
- 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
+ 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.
452
452
 
453
453
  ### Per-Project Configuration
454
454
 
@@ -590,9 +590,9 @@ describe("incrementUserAge resolver", () => {
590
590
  Once the logic gets more involved, mocking raw SQL calls becomes brittle. Push database access behind a `DbOperations` interface and test the pure function that uses it:
591
591
 
592
592
  ```typescript
593
+ import type { Namespace } from "../generated/db";
593
594
  // src/resolver/decrementUserAge.ts
594
595
  import type { Selectable } from "@tailor-platform/sdk/kysely";
595
- import type { Namespace } from "../generated/db";
596
596
 
597
597
  export interface DbOperations {
598
598
  transaction: <T>(fn: (ops: DbOperations) => Promise<T>) => Promise<T>;
@@ -690,7 +690,7 @@ describe("upsertUsers resolver", () => {
690
690
 
691
691
  Reach for [`mockTailordb`](#mocking-the-tailordb-client) instead when you want to drive the raw query sequence at the `tailordb.Client` level rather than at the Kysely layer, or [`mockTailordbWithPGlite`](#real-sql-execution-with-pglite-mocktailordbwithpglite) to execute the queries against a real in-memory Postgres.
692
692
 
693
- TailorDB migration scripts (`migrate.ts`) are unit-tested the same way: the generated `db.ts` exports the `Database` interface to type the mock, and `tailor tailordb migration script <N> --with-test` scaffolds a ready-to-fill test. To execute a migration script against real rows in an in-memory Postgres, use `createKyselyPGlite` with `@electric-sql/pglite` and the generated `db.pglite.ts` schema. See [Testing Migrations Locally](./services/tailordb-migration.md#testing-migrations-locally).
693
+ TailorDB migration scripts (`migrate.ts`) are unit-tested the same way: the generated `db.ts` exports the `Database` interface to type the mock, and `tailor tailordb migration script <N> --with-test` scaffolds a ready-to-fill test. To execute a migration script against real rows in an in-memory Postgres, use `createKyselyPGlite` with `@electric-sql/pglite` and the generated `db.pglite.ts` schema. See [Testing Migrations Locally](./services/tailordb/migration.md#testing-migrations-locally).
694
694
 
695
695
  #### Resolvers that resume a workflow
696
696