@tailor-platform/sdk 2.14.2 → 2.16.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 (129) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/bin/tailor.mjs +2 -2
  3. package/dist/application-Dw3t9p2f.mjs +1 -0
  4. package/dist/{application-BAqZFMWP.mjs → application-wYQ-ivDg.mjs} +27 -27
  5. package/dist/application-wYQ-ivDg.mjs.map +1 -0
  6. package/dist/cli/commands/deploy/app-id-lock.d.mts +97 -0
  7. package/dist/cli/commands/executor/jobs.d.mts +1 -0
  8. package/dist/cli/commands/generate/seed/bundler.d.mts +18 -0
  9. package/dist/cli/commands/tailordb/migrate/config.d.mts +1 -0
  10. package/dist/cli/commands/tailordb/migrate/diff-calculator.d.mts +15 -1
  11. package/dist/cli/commands/tailordb/migrate/generate.d.mts +5 -2
  12. package/dist/cli/commands/tailordb/migrate/rename-detection.d.mts +13 -0
  13. package/dist/cli/commands/tailordb/migrate/snapshot-comparison.d.mts +7 -1
  14. package/dist/cli/commands/workflow/executions.d.mts +2 -0
  15. package/dist/cli/commands/workflow/waiter.d.mts +1 -0
  16. package/dist/cli/commands/workspace/create.d.mts +16 -1
  17. package/dist/cli/commands/workspace/expiry.d.mts +5 -0
  18. package/dist/cli/commands/workspace/get.d.mts +6 -1
  19. package/dist/cli/commands/workspace/list.d.mts +1 -0
  20. package/dist/cli/lib.d.mts +6 -3
  21. package/dist/cli/lib.mjs +1 -1
  22. package/dist/cli/lib.mjs.map +1 -1
  23. package/dist/cli/main.d.mts +8 -8
  24. package/dist/cli/main.mjs +56 -60
  25. package/dist/cli/main.mjs.map +1 -1
  26. package/dist/cli/shared/command.d.mts +1 -1
  27. package/dist/cli/shared/error-json.d.mts +2 -1
  28. package/dist/cli/shared/errors.d.mts +1 -0
  29. package/dist/cli/shared/function-execution.d.mts +14 -0
  30. package/dist/cli/shared/github-actions.d.mts +13 -0
  31. package/dist/completion/zsh-worker.zsh +98 -5
  32. package/dist/configure/config/types.d.mts +23 -6
  33. package/dist/configure/index.mjs +1 -1
  34. package/dist/configure/index.mjs.map +1 -1
  35. package/dist/configure/services/executor/trigger/event.d.mts +5 -2
  36. package/dist/crashreport-DF8YMIE5.mjs +1 -0
  37. package/dist/{crashreport-BN28xp5B.mjs → crashreport-Doz2Kuuq.mjs} +2 -2
  38. package/dist/{crashreport-BN28xp5B.mjs.map → crashreport-Doz2Kuuq.mjs.map} +1 -1
  39. package/dist/date-CGBZbMW5.mjs +2 -0
  40. package/dist/date-CGBZbMW5.mjs.map +1 -0
  41. package/dist/errors-BtTxkzgy.mjs +7 -0
  42. package/dist/errors-BtTxkzgy.mjs.map +1 -0
  43. package/dist/file-DKBOj5q4.mjs +2 -0
  44. package/dist/file-DKBOj5q4.mjs.map +1 -0
  45. package/dist/guards-ForsrxnH.mjs +2 -0
  46. package/dist/guards-ForsrxnH.mjs.map +1 -0
  47. package/dist/{logger-CCjs1DuH.mjs → logger-CEAxByN5.mjs} +3 -3
  48. package/dist/{logger-CCjs1DuH.mjs.map → logger-CEAxByN5.mjs.map} +1 -1
  49. package/dist/manager-E3ffRcCt.mjs +2 -0
  50. package/dist/manager-E3ffRcCt.mjs.map +1 -0
  51. package/dist/node-builtins-DYfhPBnz.mjs +2 -0
  52. package/dist/node-builtins-DYfhPBnz.mjs.map +1 -0
  53. package/dist/plugin/builtin/file-utils/index.mjs +27 -1
  54. package/dist/plugin/builtin/file-utils/index.mjs.map +1 -1
  55. package/dist/plugin/builtin/seed/index.mjs +1 -1
  56. package/dist/plugin/builtin/seed/seed-type-processor.d.mts +17 -0
  57. package/dist/plugin/get-generated-table.d.mts +13 -0
  58. package/dist/plugin/index.d.mts +2 -2
  59. package/dist/plugin/index.mjs +1 -1
  60. package/dist/plugin/index.mjs.map +1 -1
  61. package/dist/register-ts-hook-DrHS-J1k.mjs +878 -0
  62. package/dist/register-ts-hook-DrHS-J1k.mjs.map +1 -0
  63. package/dist/runtime/file.d.mts +46 -2
  64. package/dist/runtime/file.mjs +1 -1
  65. package/dist/runtime/index.mjs +1 -1
  66. package/dist/{schema-DMlLMsWA.mjs → schema-BTioi2dP.mjs} +2 -2
  67. package/dist/{schema-DMlLMsWA.mjs.map → schema-BTioi2dP.mjs.map} +1 -1
  68. package/dist/{seed-CCc9Xk66.mjs → seed-C3P_T_Eh.mjs} +28 -9
  69. package/dist/seed-C3P_T_Eh.mjs.map +1 -0
  70. package/dist/service-Cw7hb5HK.mjs +2 -0
  71. package/dist/service-Cw7hb5HK.mjs.map +1 -0
  72. package/dist/service-DaoW0kzo.mjs +7 -0
  73. package/dist/service-DaoW0kzo.mjs.map +1 -0
  74. package/dist/service-dn9jxC8c.mjs +1 -0
  75. package/dist/service_pb-DKrsO1_u.mjs +1 -0
  76. package/dist/service_pb-DprmLNsq.mjs +2 -0
  77. package/dist/{service_pb-BdzjiHfC.mjs.map → service_pb-DprmLNsq.mjs.map} +1 -1
  78. package/dist/types/auth.generated.d.mts +1 -19
  79. package/dist/types/executor.generated.d.mts +5 -3
  80. package/dist/types/field.generated.d.mts +40 -0
  81. package/dist/types/helpers.d.mts +9 -1
  82. package/dist/types/resolver.generated.d.mts +5 -40
  83. package/dist/utils/test/index.d.mts +7 -5
  84. package/dist/utils/test/index.mjs +1 -1
  85. package/dist/utils/test/index.mjs.map +1 -1
  86. package/dist/vitest/index.d.mts +2 -3
  87. package/dist/vitest/index.mjs +1 -1
  88. package/dist/vitest/index.mjs.map +1 -1
  89. package/dist/vitest/mocks/file.d.mts +2 -2
  90. package/dist/vitest/setup.d.mts +0 -37
  91. package/dist/vitest/setup.mjs +1 -1
  92. package/dist/vitest/setup.mjs.map +1 -1
  93. package/dist/{workspace_resource_pb-C4EY-Gns.mjs → workspace_resource_pb-BOoRts_z.mjs} +2 -2
  94. package/dist/{workspace_resource_pb-C4EY-Gns.mjs.map → workspace_resource_pb-BOoRts_z.mjs.map} +1 -1
  95. package/docs/cli/application.md +1 -1
  96. package/docs/cli/function.md +19 -6
  97. package/docs/cli/tailordb.md +11 -11
  98. package/docs/cli/workspace.md +133 -20
  99. package/docs/cli-reference.md +94 -25
  100. package/docs/configuration.md +20 -3
  101. package/docs/github-actions.md +45 -13
  102. package/docs/migration/v3.md +38 -0
  103. package/docs/plugin/custom.md +36 -4
  104. package/docs/runtime.md +39 -0
  105. package/docs/services/resolver.md +3 -1
  106. package/docs/services/tailordb-migration.md +31 -9
  107. package/docs/testing.md +3 -7
  108. package/package.json +10 -10
  109. package/dist/application-BAqZFMWP.mjs.map +0 -1
  110. package/dist/application-BKlOiD1x.mjs +0 -1
  111. package/dist/crashreport-By23O2k-.mjs +0 -1
  112. package/dist/date-DrUO8rOJ.mjs +0 -2
  113. package/dist/date-DrUO8rOJ.mjs.map +0 -1
  114. package/dist/errors-BlX4gUw5.mjs +0 -4
  115. package/dist/errors-BlX4gUw5.mjs.map +0 -1
  116. package/dist/file-COPYfju_.mjs +0 -2
  117. package/dist/file-COPYfju_.mjs.map +0 -1
  118. package/dist/node-builtins-TQHhwNzG.mjs +0 -2
  119. package/dist/node-builtins-TQHhwNzG.mjs.map +0 -1
  120. package/dist/register-ts-hook-Cphn4r8s.mjs +0 -642
  121. package/dist/register-ts-hook-Cphn4r8s.mjs.map +0 -1
  122. package/dist/seed-CCc9Xk66.mjs.map +0 -1
  123. package/dist/service-7SCy2bhm.mjs +0 -7
  124. package/dist/service-7SCy2bhm.mjs.map +0 -1
  125. package/dist/service-CWFQ8EVo.mjs +0 -1
  126. package/dist/service-CqbQXFDp.mjs +0 -2
  127. package/dist/service-CqbQXFDp.mjs.map +0 -1
  128. package/dist/service_pb-BdzjiHfC.mjs +0 -2
  129. package/dist/service_pb-p68oLtp9.mjs +0 -1
@@ -324,6 +324,37 @@ const AuditLog = await getGeneratedTable(configPath, "@example/audit-log", null,
324
324
  5. Caches the result to avoid redundant processing
325
325
  6. Returns the generated table matching the specified kind
326
326
 
327
+ ## getExtendedTable Helper
328
+
329
+ A table that plugins are attached to gains the fields those plugins return in `extends.fields`, but only in the table `tailor generate` registers — the object exported from the table's source file stays as written. `getExtendedTable()` returns the table with every plugin-added field applied, so tooling that reads the table at runtime sees the same fields `tailor generate` does.
330
+
331
+ ```typescript
332
+ import { join } from "node:path";
333
+ import { getExtendedTable } from "@tailor-platform/sdk/plugin";
334
+ import { customer } from "./tailordb/customer";
335
+
336
+ const configPath = join(import.meta.dirname, "./tailor.config.ts");
337
+
338
+ const extendedCustomer = await getExtendedTable(configPath, customer);
339
+ extendedCustomer.fields.deletedAt; // added by a plugin attached with .plugin()
340
+ ```
341
+
342
+ **Parameters:**
343
+
344
+ - `configPath`: Path to `tailor.config.ts` (absolute or relative to cwd)
345
+ - `sourceTable`: The TailorDB table as exported from its source file
346
+
347
+ **How it works:**
348
+
349
+ 1. Returns `sourceTable` itself when no plugin is attached to it
350
+ 2. Loads and caches the config from the given path
351
+ 3. Auto-resolves the namespace from config
352
+ 4. Calls each attached plugin's `onTableLoaded()` in the order of the `.plugin()` calls, each seeing the fields the plugins before it added
353
+ 5. Caches the result per config path and table
354
+ 6. Returns a new table with the added fields; `sourceTable` is not changed
355
+
356
+ The seed schema files `tailor generate` writes for tables with plugins attached use this helper, so `tailor seed validate` checks plugin-added fields like the table's own.
357
+
327
358
  ## Examples
328
359
 
329
360
  ### Definition-time Plugin (Soft Delete)
@@ -646,10 +677,11 @@ of whether `.files()` or `.plugin()` was called first. `tailor generate` also re
646
677
  collision at runtime, as a backstop for any case a table's static type doesn't otherwise catch.
647
678
 
648
679
  This only affects the table's static type. The corresponding field exists on the table's
649
- generated schema, and on the table object's own `fields`, only after `tailor generate` actually
650
- applies `extends.fields`. Before that, reading an injected field directly off the table
651
- (`table.fields.status`) returns `undefined`, and `pickFields(["status"])` throws — call these only
652
- with the table's originally declared fields, not ones a plugin injects.
680
+ generated schema, and on the table `tailor generate` registers, only once `extends.fields` is
681
+ applied; the table object exported from the source file never gains it. Reading an injected field
682
+ directly off that object (`table.fields.status`) returns `undefined`, and `pickFields(["status"])`
683
+ throws — call these only with the table's originally declared fields, or load the table with
684
+ [`getExtendedTable()`](#getextendedtable-helper) first.
653
685
 
654
686
  To keep the declared type and the runtime implementation in sync, give `Plugin`'s optional third
655
687
  type parameter the same shape and use it inside `onTableLoaded`:
package/docs/runtime.md CHANGED
@@ -43,6 +43,45 @@ const { url } = await aigateway.get("my-aigateway");
43
43
  logger.info("order processed", { orderId: "o-1", total: 99.5 });
44
44
  ```
45
45
 
46
+ ## Uploading files
47
+
48
+ Pass bytes directly to `file.upload`. For strings, specify how to interpret the input:
49
+
50
+ ```ts
51
+ import { file } from "@tailor-platform/sdk/runtime";
52
+
53
+ await file.upload("my-namespace", "Document", "attachment", recordId, text, {
54
+ encoding: "utf8",
55
+ contentType: "text/plain",
56
+ });
57
+
58
+ await file.upload("my-namespace", "Document", "attachment", recordId, imageBase64, {
59
+ encoding: "base64",
60
+ contentType: "image/png",
61
+ });
62
+ ```
63
+
64
+ `encoding` controls the input string's interpretation; `contentType` describes the stored file.
65
+ Setting `contentType: "image/png"` does not decode Base64. Byte arrays and buffers are uploaded
66
+ unchanged, even when `encoding` is supplied. Omitting `contentType` with `encoding: "utf8"`
67
+ stores the file as `text/plain; charset=utf-8`; omitting it with `"base64"` leaves the content
68
+ type unset, unless the Base64 string is a `data:<contentType>;base64,<data>` URL, in which case
69
+ `<contentType>` is used. An explicit `contentType` option always takes precedence over one
70
+ found in a data URL.
71
+
72
+ Base64 input may omit padding and contain ASCII whitespace. Invalid characters and invalid
73
+ padding are rejected with `TypeError` before upload. Decoding Base64 does not validate the
74
+ resulting file's format.
75
+
76
+ Uploading a string without `encoding` still stores it as text, but that overload is deprecated
77
+ and will be removed in v3. Add `encoding: "utf8"` to preserve existing behavior, or choose
78
+ `"base64"` when decoding is intended. Calls with byte arrays or buffers are not deprecated.
79
+ The generated `uploadFile` helper supports the same options; run `tailor generate` to update it.
80
+
81
+ The encoding option is available on the imported SDK `file.upload` and generated helpers.
82
+ For code using the global `tailordb.file.upload`, switch to the imported `file.upload` before
83
+ using this option.
84
+
46
85
  ## Subpath imports
47
86
 
48
87
  Each namespace can also be imported individually so you only pull what you need:
@@ -139,7 +139,9 @@ createResolver({
139
139
 
140
140
  GraphQL still accepts and returns `YYYY-MM-DD` strings. The SDK converts input to a `Date` at midnight UTC and formats output using its UTC year, month, and day. Use UTC getters and setters for date arithmetic; local getters and setters depend on the runtime's timezone. Any time component in the returned `Date` is discarded according to UTC, so `new Date("2026-09-07T00:00:00+09:00")` returns `"2026-09-06"`.
141
141
 
142
- This option also works in nested objects and with `array: true` or `optional: true`. Input must be a valid calendar date, and output must be a valid `Date` with a UTC year between 0000 and 9999. Both deployed resolvers and `tailor function run` perform these conversions.
142
+ This option also works in nested objects and with `array: true` or `optional: true`. Input must be a valid calendar date, and output must be a valid `Date` with a 4-digit UTC year (0000-9999). Both deployed resolvers and `tailor function run` perform these conversions.
143
+
144
+ An executor subscribing to the resolver with `resolverExecutedTrigger` receives the event as JSON, so `result` holds the `YYYY-MM-DD` string rather than a `Date`.
143
145
 
144
146
  ### Custom Type Name (`typeName`)
145
147
 
@@ -164,15 +164,33 @@ During deploy, the pre-migration phase keeps the old field and adds the new fiel
164
164
 
165
165
  If you decline the prompt (or confirm the removal with `--drop`), the change stays a plain removal + addition with the usual data-loss warning.
166
166
 
167
- Renaming a member inside a **nested field** is not detected as a rename: `User.address.zip` → `zipCode` becomes a single `field_modified` on `address`, so no `field_renamed` change is recorded and no copy script is generated. The removed member is reported as a data-loss warning instead, which `migration validate --strict` picks up like any other warning; when a compatible member was added at the same level, the warning names it as a possible rename target:
167
+ #### Renaming a member inside a nested field
168
+
169
+ Members inside a **nested field** (`db.object(...)`) are detected the same way: when `migration generate` finds a member removed from a nested field and a compatible member added under the same parent, it asks whether the member was renamed. Two members qualify only when copying the value preserves it exactly, because nested member constraints other than the new member's requiredness and unique constraint are not relaxed: the type, array-ness, requiredness, foreign key target, decimal scale, hooks, and validations must match (index, unique, and vector may differ, as for a top-level rename), enum values may be added but not removed, an object-typed member must keep the same members recursively, and serial members never qualify.
170
+
171
+ ```
172
+ ? User.address.zip was removed and zipCode was added with a compatible type. Was it renamed to zipCode? (Y/n)
173
+ ```
174
+
175
+ In non-interactive environments the command fails while a candidate is left unresolved, exactly like field renames. Resolve it with the nested member forms of the same flags (a value with two or more dots before the `:` targets a member; deeper members use their dotted path, and the new name is a single segment under the same parent):
176
+
177
+ ```bash
178
+ tailor tailordb migration generate --rename "User.address.zip:zipCode"
179
+ tailor tailordb migration generate --rename "User.address.geo.lat:latitude"
180
+ tailor tailordb migration generate --drop "User.address.zip"
181
+ ```
182
+
183
+ A confirmed rename is recorded on the nested field's `field_modified` change as `memberRenames` and treated as **breaking**, so a migration script is required. The generated `migrate.ts` reads every row's nested value, stores each renamed member under its new name (descending into arrays at every level), and writes the value back; the old member is kept in the written value because it stays on the schema until the post-migration phase drops it. During deploy, the pre-migration phase keeps the old member on the nested field and adds the new member as optional, the script copies the values, and the post-migration phase drops the old member and enforces the new member's requiredness.
184
+
185
+ A member removed without a confirmed rename stays a data-loss warning, which `migration validate --strict` picks up like any other warning; when a compatible sibling was added, the warning names it and the `--rename` value that confirms the rename:
168
186
 
169
187
  ```
170
188
  Warning: data loss possible:
171
189
 
172
- - User.address.zip: Nested member removed (existing values will no longer be accessible through the schema). Possibly renamed to zipCode: nested renames are not detected, so copy its values with a migration script if it was renamed
190
+ - User.address.zip: Nested member removed (existing values will no longer be accessible through the schema). Possibly renamed to zipCode: confirm it with --rename "User.address.zip:<newName>" to scaffold a copy script, or keep the removal and copy the values yourself
173
191
  ```
174
192
 
175
- To carry the values over, add a custom `tailordb migration script` to the migration. The pre-migration phase keeps the removed member on the nested field until the script finishes, exactly like a removed top-level field, so the script can read `zip` and write `zipCode`. Nested fields reach the script as objects, so the copy rewrites the whole `address` value; keep the old member in what you write, because it is still part of the schema until the post-migration phase drops it. Declare the new member as optional: the platform rejects a nested member that is required while existing records lack it, and nested member constraints are not relaxed during the pre-migration phase.
193
+ The pre-migration phase keeps a removed member on the nested field until the script finishes, exactly like a removed top-level field, so a custom script can still read it. Nested fields reach the script as objects. Renaming a nested member cannot be combined in one migration with renaming its table or the nested field itself, and an object-typed member cannot be renamed in the same migration as one of its own members; split such changes into separate migrations (rename the object first, then its member).
176
194
 
177
195
  ### Renaming a table
178
196
 
@@ -261,7 +279,7 @@ export default defineConfig({
261
279
 
262
280
  ### Migration file format compatibility
263
281
 
264
- Migration files are versioned independently of the SDK package. This SDK writes format version `3` and reads versions `1` through `3`. It normalizes supported older formats in memory; it never rewrites applied migration files on disk.
282
+ Migration files are versioned independently of the SDK package. This SDK writes format version `6` and reads versions `1` through `6`. 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.
265
283
 
266
284
  If a future SDK can no longer replay an old migration format, re-baseline while using an SDK version that still supports the complete history, commit the new baseline, deploy it to every environment, and then upgrade the SDK. A file from a newer unsupported format instead requires upgrading the SDK first. The CLI rejects both cases with guidance rather than attempting a best-effort replay.
267
285
 
@@ -348,6 +366,8 @@ The `env` values are injected at bundle time (the same mechanism as resolvers/ex
348
366
  | Add required field | Yes | Yes | Script populates default values |
349
367
  | Remove field | No | Optional | Warning tier — no script is auto-generated, but you can add one with `tailordb migration script` to preserve or clear data before the field leaves the active schema. The field stays readable from `migrate.ts` during Pre-migration. |
350
368
  | Rename field | Yes | Yes | Confirmed interactively at generate time or via `--rename "Table.oldField:newField"` — see [Renaming a field](#renaming-a-field). Auto-generated script copies values from the old field to the new one; both fields coexist during Pre-migration. |
369
+ | Remove nested member | No | Optional | Warning tier — see [Renaming a member inside a nested field](#renaming-a-member-inside-a-nested-field). The member stays readable from `migrate.ts` during Pre-migration. |
370
+ | Rename nested member | Yes | Yes | Confirmed interactively at generate time or via `--rename "Table.field.oldMember:newMember"`. Auto-generated script rewrites each row's nested value; the old member stays readable and the new member is optional during Pre-migration. |
351
371
  | Change optional → required | Yes | Yes | Script sets defaults for null values |
352
372
  | Change required → optional | No | No | Schema change only |
353
373
  | Add index (non-unique) | No | No | Schema change only |
@@ -366,7 +386,7 @@ The `env` values are injected at bundle time (the same mechanism as resolvers/ex
366
386
  | Change field type (verified pair) | Yes | Yes | In-place for the pairs listed under [Field type changes](#field-type-changes); review the generated normalization scaffold and customize it only when existing values need transformation |
367
387
  | Change field type (other pair) | Yes | Yes | Two migrations, generated together after you confirm — see [Converting a field type](#converting-a-field-type). Edit the conversion in the first; the second needs no changes. |
368
388
  | Change array → single value | - | - | **Not supported** — see [Converting a field type](#converting-a-field-type) |
369
- | Change single value → array | - | - | **Not supported** — see [Converting a field type](#converting-a-field-type) |
389
+ | Change single value → array | Yes | Yes | Two migrations, generated together after you confirm — see [Converting a field type](#converting-a-field-type). Each stored value becomes a one-element array, so the conversion needs no edits. |
370
390
 
371
391
  ### Field type changes
372
392
 
@@ -440,7 +460,7 @@ The generated `never` annotation intentionally causes a TypeScript error until y
440
460
 
441
461
  ### Converting a field type
442
462
 
443
- A field type change outside the verified in-place pairs — `string` → `integer`, for example — cannot be applied in one step, because the field would have to hold both shapes at once. `migration generate` offers to carry the values through a temporary field instead:
463
+ A field type change outside the verified in-place pairs — `string` → `integer`, for example — cannot be applied in one step, because the field would have to hold both shapes at once. The same holds when a single value becomes an array (`string` → `string[]`): the stored values must be rewritten as arrays before the field can take the new shape. `migration generate` offers to carry the values through a temporary field instead:
444
464
 
445
465
  ```
446
466
  User.price changes from string to integer, which cannot be applied in one step.
@@ -449,9 +469,11 @@ User.price changes from string to integer, which cannot be applied in one step.
449
469
 
450
470
  Confirming writes two migrations:
451
471
 
452
- 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.
472
+ 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.
453
473
  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.
454
474
 
475
+ 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.
476
+
455
477
  `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.
456
478
 
457
479
  The original field is removed in the first migration rather than the second, because the rename needs its name free. Your script can still read it while the conversion runs.
@@ -470,7 +492,7 @@ Without the flag the command fails rather than converting anything, so a scripte
470
492
 
471
493
  Some changes are still rejected and need a temporary field you add yourself — add the new field, write a script that fills it and clears the old one, then remove the old field and rename the temporary one in a later migration:
472
494
 
473
- - Array-to-scalar and scalar-to-array, since collapsing an array has no answer the generated script could choose for you.
495
+ - A field that is already an array: collapsing it into a single value has no answer the generated script could choose for you, and changing its element type (`string[]` → `integer[]`) is not generated either.
474
496
  - A field that is unique, or that an index, relationship, permission, or table-level script names. Those keep pointing at the original name, which the conversion removes.
475
497
 
476
498
  ## Testing Pending Migrations
@@ -552,7 +574,7 @@ When you run `tailor deploy`, the SDK detects pending migrations (anything past
552
574
 
553
575
  For each pending migration:
554
576
 
555
- 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. 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.
577
+ 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.
556
578
  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).
557
579
  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.
558
580
  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.
package/docs/testing.md CHANGED
@@ -59,7 +59,7 @@ export default defineConfig({
59
59
  `tailorRuntime()` provides:
60
60
 
61
61
  1. **Node.js module blocking** — `import { randomBytes } from "node:crypto"` in production code throws an error with a suggestion for the Web Standard API alternative (`globalThis.crypto`). Test files (`*.test.ts`, `*.spec.ts`) are exempt.
62
- 2. **Node.js globals removal** — Only globals available in the platform runtime are kept (whitelist). `Buffer`, `global`, `setImmediate`, `__dirname`, `__filename`, `performance`, and others are removed.
62
+ 2. **Node.js globals removal** — Only globals available in the platform runtime are kept (whitelist). `Buffer`, `global`, `setImmediate`, `__dirname`, `__filename`, and others are removed.
63
63
  3. **Platform API mocks** — the platform error classes (`TailorErrors`, `TailorDBFileError`) and `tailor.context` are always available. The other namespaces (`tailordb.Client`, `tailor.workflow`, `tailor.secretmanager`, …) are mocked when you acquire the corresponding `mockX()` — see below.
64
64
 
65
65
  ### Acquiring mocks with `using`
@@ -452,12 +452,7 @@ export default defineConfig({
452
452
  plugins: [tailorRuntime()],
453
453
  test: {
454
454
  projects: [
455
- // `extends: true` is required so each project inherits the root-level
456
- // `tailorRuntime()` plugin (transform hook + injected setup file).
457
- // Without it, only the environment name rewrite applies — node:* import
458
- // blocking and per-test global cleanup will silently not run.
459
455
  {
460
- extends: true,
461
456
  test: {
462
457
  name: "unit",
463
458
  environment: "tailor-runtime",
@@ -465,7 +460,6 @@ export default defineConfig({
465
460
  },
466
461
  },
467
462
  {
468
- extends: true,
469
463
  test: {
470
464
  name: "e2e",
471
465
  include: ["e2e/**/*.test.ts"],
@@ -477,6 +471,8 @@ export default defineConfig({
477
471
  });
478
472
  ```
479
473
 
474
+ Inline projects inherit the root-level `tailorRuntime()` plugin by default on Vitest 5. On Vitest 4, add `extends: true` to each project; without it, `node:*` import blocking silently does not run.
475
+
480
476
  ### Known Limitations
481
477
 
482
478
  - **`process` and `require`** are not removed or blocked. Vitest's internal runner depends on them extensively. On the real platform runtime, they do not exist.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tailor-platform/sdk",
3
- "version": "2.14.2",
3
+ "version": "2.16.0",
4
4
  "description": "Tailor Platform SDK - The SDK to work with Tailor Platform",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -153,8 +153,8 @@
153
153
  "@badgateway/oauth2-client": "3.3.1",
154
154
  "@bufbuild/protobuf": "2.14.1",
155
155
  "@bufbuild/protovalidate": "1.2.0",
156
- "@connectrpc/connect": "2.1.2",
157
- "@connectrpc/connect-node": "2.1.2",
156
+ "@connectrpc/connect": "2.2.0",
157
+ "@connectrpc/connect-node": "2.2.0",
158
158
  "@inquirer/core": "12.0.2",
159
159
  "@inquirer/prompts": "8.7.1",
160
160
  "@jridgewell/trace-mapping": "0.3.31",
@@ -165,21 +165,22 @@
165
165
  "@opentelemetry/sdk-trace-node": "2.11.0",
166
166
  "@opentelemetry/semantic-conventions": "1.43.0",
167
167
  "@oxc-project/types": "0.148.0",
168
+ "@politty/zod": "0.2.1",
168
169
  "@secretlint/core": "13.0.5",
169
170
  "@secretlint/secretlint-rule-preset-recommend": "13.0.5",
170
171
  "@standard-schema/spec": "1.1.0",
171
172
  "@tailor-platform/function-kysely-tailordb": "0.1.3",
172
- "@toiroakr/lines-db": "0.12.6",
173
+ "@toiroakr/lines-db": "0.12.7",
173
174
  "@toiroakr/read-multiline": "0.4.1",
174
175
  "@urql/core": "6.0.3",
175
176
  "amaro": "1.1.11",
176
- "confbox": "0.2.4",
177
+ "confbox": "0.3.1",
177
178
  "date-fns": "4.4.0",
178
179
  "es-toolkit": "1.52.0",
179
180
  "find-up-simple": "1.0.1",
180
181
  "get-east-asian-width": "1.6.0",
181
182
  "get-tsconfig": "4.14.3",
182
- "globals": "17.11.0",
183
+ "globals": "17.12.0",
183
184
  "graphql": "17.0.2",
184
185
  "inflection": "3.0.2",
185
186
  "kysely": "0.29.5",
@@ -190,7 +191,6 @@
190
191
  "pathe": "2.0.3",
191
192
  "pgsql-ast-parser": "12.0.2",
192
193
  "pkg-types": "2.3.2",
193
- "politty": "0.11.9",
194
194
  "rolldown": "1.2.7",
195
195
  "semver": "7.8.5",
196
196
  "sql-highlight": "6.1.0",
@@ -208,7 +208,7 @@
208
208
  "@types/node": "24.13.3",
209
209
  "@types/semver": "7.8.0",
210
210
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
211
- "@vitest/coverage-v8": "4.1.11",
211
+ "@vitest/coverage-v8": "5.0.0",
212
212
  "eslint-plugin-zod": "4.12.0",
213
213
  "oxfmt": "0.66.0",
214
214
  "oxlint": "1.81.0",
@@ -216,8 +216,8 @@
216
216
  "sonda": "0.14.0",
217
217
  "tsdown": "0.23.0",
218
218
  "typescript": "6.0.3",
219
- "vitest": "4.1.11",
220
- "zinfer": "0.2.8"
219
+ "vitest": "5.0.0",
220
+ "zinfer": "0.4.5"
221
221
  },
222
222
  "peerDependencies": {
223
223
  "@electric-sql/pglite": ">=0.2.0",