@tailor-platform/sdk 2.35.0 → 2.36.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.
- package/CHANGELOG.md +36 -0
- package/dist/{application-DIsQSnj3.mjs → application-BbORWCj-.mjs} +16 -16
- package/dist/application-BbORWCj-.mjs.map +1 -0
- package/dist/{application-Bj2MtL-d.mjs → application-DaJbleMi.mjs} +1 -1
- package/dist/cli/commands/workspace/create.d.mts +4 -2
- package/dist/cli/lib.mjs +1 -1
- package/dist/cli/main.mjs +47 -47
- package/dist/cli/main.mjs.map +1 -1
- package/dist/completion/zsh-worker.zsh +1 -1
- package/dist/configure/index.mjs.map +1 -1
- package/dist/configure/services/executor/trigger/webhook.d.mts +1 -0
- package/dist/configure/services/tailordb/schema.d.mts +6 -2
- package/dist/{register-ts-hook-CQ7shKO5.mjs → register-ts-hook-iAcG1dfz.mjs} +72 -70
- package/dist/register-ts-hook-iAcG1dfz.mjs.map +1 -0
- package/dist/schema-BU1aIQ63.mjs.map +1 -1
- package/dist/service-DTrVVaDJ.mjs +1 -0
- package/dist/{service-eeXdVrDK.mjs → service-hIarp2Gl.mjs} +3 -3
- package/dist/service-hIarp2Gl.mjs.map +1 -0
- package/dist/vitest/index.mjs +1 -1
- package/dist/vitest/index.mjs.map +1 -1
- package/dist/vitest/mocks/file.d.mts +1 -1
- package/dist/vitest/pglite-kysely.d.mts +5 -0
- package/docs/cli/application.md +1 -1
- package/docs/cli/tailordb.md +1 -1
- package/docs/cli/user.md +1 -1
- package/docs/cli/workspace.md +1 -1
- package/docs/cli-reference.md +5 -3
- package/docs/services/executor.md +12 -5
- package/docs/services/tailordb/index.md +5 -1
- package/docs/services/tailordb/migration.md +20 -9
- package/package.json +10 -10
- package/dist/application-DIsQSnj3.mjs.map +0 -1
- package/dist/register-ts-hook-CQ7shKO5.mjs.map +0 -1
- package/dist/service-DEq_hdht.mjs +0 -1
- package/dist/service-eeXdVrDK.mjs.map +0 -1
|
@@ -45,9 +45,9 @@ export declare function mockFile(options?: MockFileOptions): {
|
|
|
45
45
|
calls: FileCall[];
|
|
46
46
|
clear(): void;
|
|
47
47
|
reset(): void;
|
|
48
|
-
delete: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<void>>;
|
|
49
48
|
download: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<FileDownloadResponse>>;
|
|
50
49
|
downloadAsBase64: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<FileDownloadAsBase64Response>>;
|
|
50
|
+
delete: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<void>>;
|
|
51
51
|
getMetadata: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<FileMetadata>>;
|
|
52
52
|
downloadStream: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<FileDownloadStreamResponse>>;
|
|
53
53
|
uploadStream: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string, readableStream: ReadableStream<Uint8Array | ArrayBuffer>, options?: FileUploadStreamOptions) => Promise<FileUploadResponse>>;
|
|
@@ -64,6 +64,11 @@ export interface CreateKyselyPGliteOptions {
|
|
|
64
64
|
* PGlite runs full PostgreSQL while TailorDB supports a subset of it, so a
|
|
65
65
|
* statement passing here can still be rejected by the platform; keep a
|
|
66
66
|
* statement-level test (see `createKyselyMock`) alongside.
|
|
67
|
+
*
|
|
68
|
+
* While a transaction is open, other queries on the returned Kysely instance
|
|
69
|
+
* wait for it to finish. Inside the transaction, run every query on the
|
|
70
|
+
* transaction (`trx`): a query on the returned instance there waits on itself
|
|
71
|
+
* and never finishes.
|
|
67
72
|
* @param client - A `PGlite` instance from `@electric-sql/pglite`
|
|
68
73
|
* @param options - Adapter options
|
|
69
74
|
* @returns A Kysely instance that executes queries on the client and closes it on `destroy()`
|
package/docs/cli/application.md
CHANGED
|
@@ -82,7 +82,7 @@ See [Global Options](../cli-reference.md#global-options) for options available t
|
|
|
82
82
|
|
|
83
83
|
**Notes**
|
|
84
84
|
|
|
85
|
-
Workspace creation uses the selected user's default organization and folder set by `user update` when neither location is given and TAILOR_PLATFORM_TOKEN is not set. Giving --organization-id or --folder-id replaces both defaults. Organization and folder IDs loaded by --env-file also replace the defaults; command-line options take precedence, and empty env-file values are ignored. With --create-workspace, an existing workspace must match the resolved name, region, organization, and folder to be reused.
|
|
85
|
+
Every workspace belongs to an organization; when none is given or set as a default, interactive runs ask for one and non-interactive runs fail. Workspace creation uses the selected user's default organization and folder set by `user update` when neither location is given and TAILOR_PLATFORM_TOKEN is not set. Giving --organization-id or --folder-id replaces both defaults. Organization and folder IDs loaded by --env-file also replace the defaults; command-line options take precedence, and empty env-file values are ignored. With --create-workspace, an existing workspace must match the resolved name, region, organization, and folder to be reused.
|
|
86
86
|
|
|
87
87
|
**JSON result:**
|
|
88
88
|
|
package/docs/cli/tailordb.md
CHANGED
|
@@ -288,7 +288,7 @@ See [Global Options](../cli-reference.md#global-options) for options available t
|
|
|
288
288
|
|
|
289
289
|
**Notes**
|
|
290
290
|
|
|
291
|
-
Local migration files are checked for a valid layout (numbering and required files in each migration directory) and a compatible format version, and deployed migration history IDs must match the local baseline. Invalid or incompatible files, history mismatches, and metadata lookup failures are reported per namespace and make the command exit non-zero; only a not-yet-deployed namespace is treated as having no applied migrations.
|
|
291
|
+
Local migration files are checked for a valid layout (numbering and required files in each migration directory) and a compatible format version, and deployed migration history IDs must match the local baseline. Invalid or incompatible files, history mismatches, a remote checkpoint ahead of the latest local migration, and metadata lookup failures are reported per namespace and make the command exit non-zero; only a not-yet-deployed namespace is treated as having no applied migrations.
|
|
292
292
|
|
|
293
293
|
#### tailordb migration sync
|
|
294
294
|
|
package/docs/cli/user.md
CHANGED
|
@@ -330,7 +330,7 @@ See [Global Options](../cli-reference.md#global-options) for options available t
|
|
|
330
330
|
|
|
331
331
|
**Notes**
|
|
332
332
|
|
|
333
|
-
`workspace create`, workspace creation during `deploy`, and the programmatic `createWorkspace()` use the default folder, or directly use the default organization when no folder is set. Giving either an organization or a folder replaces both defaults for that run. For the CLI, use --organization-id or --folder-id, or TAILOR_PLATFORM_ORGANIZATION_ID / TAILOR_PLATFORM_FOLDER_ID. The defaults are not used while TAILOR_PLATFORM_TOKEN is set, because that token does not belong to a user logged in here.
|
|
333
|
+
`workspace create`, workspace creation during `deploy`, and the programmatic `createWorkspace()` use the default folder, or directly use the default organization when no folder is set. Every workspace belongs to an organization, so creation fails when neither the command nor these defaults give one. Giving either an organization or a folder replaces both defaults for that run. For the CLI, use --organization-id or --folder-id, or TAILOR_PLATFORM_ORGANIZATION_ID / TAILOR_PLATFORM_FOLDER_ID. The defaults are not used while TAILOR_PLATFORM_TOKEN is set, because that token does not belong to a user logged in here.
|
|
334
334
|
|
|
335
335
|
Each run replaces both defaults: --default-organization-id alone clears the default folder, --default-folder-id is accepted only together with --default-organization-id, and --default-organization-id "" clears both. The IDs are not checked against the Platform here; `workspace create` and `deploy` point back to them when the Platform cannot find them or denies access to them.
|
|
336
336
|
|
package/docs/cli/workspace.md
CHANGED
|
@@ -121,7 +121,7 @@ See [Global Options](../cli-reference.md#global-options) for options available t
|
|
|
121
121
|
|
|
122
122
|
**Notes**
|
|
123
123
|
|
|
124
|
-
Without --organization-id and --folder-id, on the command line or through TAILOR_PLATFORM_ORGANIZATION_ID / TAILOR_PLATFORM_FOLDER_ID, the workspace is created in the default organization and folder that the logged-in user set with `user update`, unless TAILOR_PLATFORM_TOKEN is set. Giving either option replaces both defaults, and --folder-id requires --organization-id.
|
|
124
|
+
Every workspace belongs to an organization. Without --organization-id and --folder-id, on the command line or through TAILOR_PLATFORM_ORGANIZATION_ID / TAILOR_PLATFORM_FOLDER_ID, the workspace is created in the default organization and folder that the logged-in user set with `user update`, unless TAILOR_PLATFORM_TOKEN is set. When none of these gives an organization, the command fails. Giving either option replaces both defaults, and --folder-id requires --organization-id.
|
|
125
125
|
|
|
126
126
|
TAILOR_PLATFORM_ORGANIZATION_ID and TAILOR_PLATFORM_FOLDER_ID are also read from --env-file. Command-line options take precedence over the corresponding environment variables. Empty values in the env file are treated as not given.
|
|
127
127
|
|
package/docs/cli-reference.md
CHANGED
|
@@ -142,9 +142,11 @@ those messages describe the workflow's own checks, which can fail even when the
|
|
|
142
142
|
|
|
143
143
|
When the failure has a known source, the annotation carries it: `seed validate` reports the
|
|
144
144
|
offending JSONL file and line, including a line that is not valid JSON, and a rejected config
|
|
145
|
-
reports its file
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
reports its file. A `.ts` or `.mts` file that cannot be parsed — the config, a TailorDB type,
|
|
146
|
+
resolver, executor, workflow, or HTTP adapter file, the file passed to `function run`, or a file one
|
|
147
|
+
of them imports — reports that file and the line it failed on. Locations are written relative to
|
|
148
|
+
`GITHUB_WORKSPACE`; a file outside it is annotated without a location rather than with a path the
|
|
149
|
+
runner cannot resolve.
|
|
148
150
|
|
|
149
151
|
For a JSONL file containing a blank line, the annotation's line and the line printed in the report
|
|
150
152
|
text differ: the annotation counts every line in the file, while the printed line counts only the
|
|
@@ -505,7 +505,7 @@ interface WebhookContext<T = unknown> {
|
|
|
505
505
|
body: T; // Parsed request body
|
|
506
506
|
headers: Record<string, string>; // Request headers
|
|
507
507
|
method: "POST" | "GET" | "PUT" | "DELETE"; // HTTP method
|
|
508
|
-
rawBody: string; // Raw request body
|
|
508
|
+
rawBody: string; // Raw request body, base64-encoded
|
|
509
509
|
}
|
|
510
510
|
```
|
|
511
511
|
|
|
@@ -513,11 +513,12 @@ interface WebhookContext<T = unknown> {
|
|
|
513
513
|
|
|
514
514
|
```typescript
|
|
515
515
|
import { createExecutor, incomingWebhookTrigger } from "@tailor-platform/sdk";
|
|
516
|
+
import { secretmanager } from "@tailor-platform/sdk/runtime";
|
|
516
517
|
|
|
517
|
-
|
|
518
|
+
type StripeWebhook = {
|
|
518
519
|
type: string;
|
|
519
520
|
data: { object: { id: string; amount: number } };
|
|
520
|
-
}
|
|
521
|
+
};
|
|
521
522
|
|
|
522
523
|
export default createExecutor({
|
|
523
524
|
name: "stripe-webhook",
|
|
@@ -527,8 +528,12 @@ export default createExecutor({
|
|
|
527
528
|
}>(),
|
|
528
529
|
operation: {
|
|
529
530
|
kind: "function",
|
|
530
|
-
body: async ({ body, headers }) => {
|
|
531
|
-
const
|
|
531
|
+
body: async ({ body, headers, rawBody }) => {
|
|
532
|
+
const secret = await secretmanager.getSecret("api-keys", "stripe-webhook-secret");
|
|
533
|
+
// Implement verifyStripeSignature following https://docs.stripe.com/webhooks?verify=verify-manually#verify-manually
|
|
534
|
+
if (!(await verifyStripeSignature(headers["stripe-signature"], rawBody, secret))) {
|
|
535
|
+
throw new Error("Invalid Stripe signature");
|
|
536
|
+
}
|
|
532
537
|
console.log(`Received ${body.type} event`);
|
|
533
538
|
// Process webhook...
|
|
534
539
|
},
|
|
@@ -536,6 +541,8 @@ export default createExecutor({
|
|
|
536
541
|
});
|
|
537
542
|
```
|
|
538
543
|
|
|
544
|
+
The webhook URL is public, so verify the signature over the unparsed request body before trusting `body`. `rawBody` is delivered base64-encoded; decode it to the original bytes before computing the signature.
|
|
545
|
+
|
|
539
546
|
**With custom response:**
|
|
540
547
|
|
|
541
548
|
```typescript
|
|
@@ -177,7 +177,11 @@ db.string().unique();
|
|
|
177
177
|
|
|
178
178
|
`.unique()` and `.index()` also work on a field inside a `db.object()` that is not an array: with `.unique()` the value must be unique across rows, while `.index()` only indexes it and still allows duplicates. Inside an array of objects (`db.object(..., { array: true })`) the platform does not reject duplicate values, so both are rejected there with an error, as on an array field.
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
`.unique()` and `.index()` cannot be set on a `db.object()` field itself, whether it is a table field or a member of another `db.object()`, because the platform cannot index an object value.
|
|
181
|
+
|
|
182
|
+
A project that already uses `.unique()` or `.index()` on a field inside a `db.object()` was deployed with the setting stored as off, so its next `tailor deploy` stops with `MIGRATION_REMOTE_DRIFT`, and `tailor tailordb migration validate` fails the same way. `tailor tailordb migration generate` reports no differences. Run `tailor deploy --no-schema-check` once to apply the setting. If stored rows already hold duplicate values for a field that gains `.unique()`, that deploy fails and the schema stays unchanged until the duplicates are resolved.
|
|
183
|
+
|
|
184
|
+
A project that sets `.unique()` or `.index()` on a member that is itself a `db.object()` now fails with an error naming the field. Remove the modifier. Then, in a namespace with `migration.directory`, run `tailor tailordb migration generate` and `tailor deploy --no-schema-check` once; later deploys need no flag.
|
|
181
185
|
|
|
182
186
|
### Relations
|
|
183
187
|
|
|
@@ -181,6 +181,8 @@ The generated script includes a `TODO` for each nested constraint change. Replac
|
|
|
181
181
|
|
|
182
182
|
Changing a nested member's type, array-ness, decimal scale, or foreign key target is unsupported in a single migration. Use separate migrations to add a member with the target shape, copy or transform values in a custom script, and then remove the old member.
|
|
183
183
|
|
|
184
|
+
Adding a relation to an existing nested member is non-breaking, and its existing values are not checked. Fix values that point at no record before you deploy the migration that adds it: afterwards, updating a row that still holds one fails, even when the update changes only other values.
|
|
185
|
+
|
|
184
186
|
#### Renaming a member inside a nested field
|
|
185
187
|
|
|
186
188
|
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 a rename copies existing values without transforming them: 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.
|
|
@@ -277,10 +279,10 @@ export default defineConfig({
|
|
|
277
279
|
});
|
|
278
280
|
```
|
|
279
281
|
|
|
280
|
-
| Option | Type | Description
|
|
281
|
-
| ----------------------- | ------ |
|
|
282
|
-
| `migration.directory` | string | Directory path for migration files. Required when migrations are enabled for the namespace.
|
|
283
|
-
| `migration.machineUser` | string | Machine user used to run migration scripts. Optional; defaults to the first entry in `auth.machineUsers`.
|
|
282
|
+
| Option | Type | Description |
|
|
283
|
+
| ----------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
284
|
+
| `migration.directory` | string | Directory path for migration files. Required when migrations are enabled for the namespace. Each namespace needs its own directory. |
|
|
285
|
+
| `migration.machineUser` | string | Machine user used to run migration scripts. Optional; defaults to the first entry in `auth.machineUsers`. |
|
|
284
286
|
|
|
285
287
|
## Generated Files
|
|
286
288
|
|
|
@@ -419,7 +421,7 @@ export const steps = {
|
|
|
419
421
|
```
|
|
420
422
|
|
|
421
423
|
- A script exports either `main` or `steps`. If it exports both, `main` runs as before and `steps` is ignored with a warning from `tailor deploy` and `tailor tailordb migration validate`.
|
|
422
|
-
- Generated scripts export `steps`. Each schema change that needs a data migration gets its own step, named for what it does: `populate<Table><Field>` for an added required field (for example `populateUserEmail`), `rename<Table><Field>` for a renamed field, `copy<Old>To<New>` for a renamed table, `update<Table><Field>` for a changed field, `resolve<Table><Index>` for a unique index, and `convert<Table><Field>` for a field type conversion. When that name is not a valid step name (for example because it is longer than 64 characters), or when another step or a helper in the script already uses it, the step is named `change<N>` instead (`expand<N>` for a conversion through a temporary field), where N is the change's position in the migration; the fallback does not mean the schema-derived name was wrong. A step lists in `dependsOn` every earlier step that touches the same field (a foreign key that changes its target also touches `id`
|
|
424
|
+
- Generated scripts export `steps`. Each schema change that needs a data migration gets its own step, named for what it does: `populate<Table><Field>` for an added required field (for example `populateUserEmail`), `rename<Table><Field>` for a renamed field, `copy<Old>To<New>` for a renamed table, `update<Table><Field>` for a changed field, `resolve<Table><Index>` for a unique index, and `convert<Table><Field>` for a field type conversion. When that name is not a valid step name (for example because it is longer than 64 characters), or when another step or a helper in the script already uses it, the step is named `change<N>` instead (`expand<N>` for a conversion through a temporary field), where N is the change's position in the migration; the fallback does not mean the schema-derived name was wrong. A step lists in `dependsOn` every earlier step that touches the same field (a foreign key that is added or changes its target also touches the key it points to, `id` unless the relation sets another one), so steps that touch the same field run in the order the changes are listed and changes to different fields do not wait for each other. The copy of a renamed table's rows runs after the copies of the renamed tables its foreign keys point to, so the rows they reference already exist. It skips the rows whose ids the new table already has, so running it again does not insert them twice. A step that rewrites values that already exist (the duplicate resolution of a unique index or field, a field change, a conversion through a temporary field) has a comment above its function saying so: once it commits it cannot be undone, so check the values it writes before you deploy. Where the scaffold cannot pick a value for you (what an added required field holds in existing records, the new value of duplicates, the value that replaces a removed enum value, the new reference of a retargeted foreign key or of a value that points at no record when a field gains a relation, the copy of renamed tables that reference each other), it calls `TODO(message)`, which `./db` exports. The migration fails at that call, and `tailordb migration validate` and `tailor deploy` reject a `migrate.ts` that still calls it before anything is changed. Replace each call with the value, then remove the `TODO` import. A `--data-only` migration, and a migration with no data statements to scaffold, get a single `migrate` step; split it into several steps where the work divides.
|
|
423
425
|
- Each step is an object with a `run` function — it receives its own transaction and the same `MigrationContext` as `main` — and an optional `dependsOn` list naming the steps that must complete before it starts.
|
|
424
426
|
- Steps with no dependency between them have no guaranteed order. Declaration order is not execution order, and a future SDK version may run independent steps concurrently, so declare `dependsOn` for every ordering your steps rely on.
|
|
425
427
|
- Declare `steps` directly as `export const steps = { ... }`, with each step written inside it as an object literal with a literal name and `dependsOn` list. Step names start with a letter and contain only letters, digits, and underscores, up to 64 characters. `tailor tailordb migration validate` and `tailor deploy` reject unknown dependencies and cycles before anything is changed.
|
|
@@ -519,6 +521,7 @@ The same command settles a migration whose script run a deploy could not confirm
|
|
|
519
521
|
| Remove table | No | Optional | Warning tier — no script is auto-generated, but you can add one with `tailordb migration script` to preserve data before the table leaves the active schema. The table stays readable from `migrate.ts` during Pre-migration. |
|
|
520
522
|
| Rename table | Yes | Yes | Confirmed interactively at generate time or via `--rename "OldTable:NewTable"` — see [Renaming a table](#renaming-a-table). Auto-generated script copies all rows preserving ids; both tables coexist until post-migration cleanup. |
|
|
521
523
|
| Change foreign key target table | Yes | Yes | Script updates references to the new target |
|
|
524
|
+
| Add relation to existing field | Yes | Yes | Script must resolve values that point at no record. The foreign key is withheld until Post-migration. A relation added to a nested member is non-breaking — see [Changing nested member constraints](#changing-nested-member-constraints). |
|
|
522
525
|
| 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 |
|
|
523
526
|
| 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. |
|
|
524
527
|
| Change array → single value | - | - | **Not supported** — see [Converting a field type](#converting-a-field-type) |
|
|
@@ -720,7 +723,7 @@ When you run `tailor deploy`, the SDK detects pending migrations (anything past
|
|
|
720
723
|
|
|
721
724
|
For each pending migration:
|
|
722
725
|
|
|
723
|
-
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. Added or newly required nested members are optional and removed nested enum values remain allowed until Post-migration. 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.
|
|
726
|
+
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. Added or newly required nested members are optional and removed nested enum values remain allowed until Post-migration. 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. A foreign key added to an existing top-level field is withheld until Post-migration, so `migrate.ts` can update rows whose values point at no record; Post-migration adds it without checking the stored values. 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.
|
|
724
727
|
2. **Script execution**: If `migrate.ts` exists on disk for this migration, it is bundled and runs on the platform as the configured machine user inside a transaction — or, for a script that exports [`steps`](#splitting-a-migration-into-steps), one transaction per step. The script is hard-required for breaking changes (`diff.requiresMigrationScript`) — deploy fails if the file is missing, unless a `--no-script` acknowledgment was recorded (see [Breaking changes without a script](#breaking-changes-without-a-script)). It is also executed when present for warning-tier diffs — see [Warnings and optional migration scripts](#warnings-and-optional-migration-scripts).
|
|
725
728
|
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.
|
|
726
729
|
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.
|
|
@@ -758,7 +761,7 @@ Maintenance mode restricts GraphQL writes and record events only. Resolvers, exe
|
|
|
758
761
|
- A [multi-step migration](#splitting-a-migration-into-steps) left in progress keeps its tables in maintenance mode until a deploy completes it.
|
|
759
762
|
- A migration whose script run a deploy could not confirm keeps its tables in maintenance mode until you settle it with `tailor tailordb migration resolve` (see [Failure Recovery](#failure-recovery)).
|
|
760
763
|
- With `"deploy"`, if a later part of the deploy fails, the tables leave maintenance mode before the error is reported. When the deploy ends maintenance mode, it leaves the tables of a namespace unchanged if that namespace's checkpoint number or migration history changed concurrently, or if checkpoint ownership cannot be verified, and reports it.
|
|
761
|
-
- If the deploy process is interrupted after a namespace's migrations completed but before maintenance mode ended, that namespace's tables stay in maintenance mode until a later `tailor deploy` releases them when it applies its TailorDB changes, before it replaces resolvers and executors. A namespace whose migrations had not all completed is not released this way: the next deploy resumes a multi-step migration left in progress, stops at a migration whose script was running until you settle it, and otherwise reports remote schema drift (see [Failure Recovery](#failure-recovery)).
|
|
764
|
+
- If the deploy process is interrupted after a namespace's migrations completed but before maintenance mode ended, that namespace's tables stay in maintenance mode until a later `tailor deploy` releases them when it applies its TailorDB changes, before it replaces resolvers and executors. A namespace whose migrations had not all completed is not released this way: the next deploy resumes a multi-step migration left in progress, stops at a migration whose script was running until you settle it, and otherwise reports remote schema drift (see [Failure Recovery](#failure-recovery)), except when an earlier checkpoint of the interrupted deploy recorded the maintenance mode. Every migration checkpoint of a namespace records it, so with pending migrations `0001` and `0002`, a deploy interrupted right after the checkpoint of `0001` does not report drift: the next deploy keeps the tables in maintenance mode while it applies `0002` and releases them afterwards (with `"deploy"`, after the whole deploy).
|
|
762
765
|
|
|
763
766
|
#### Keeping maintenance mode short
|
|
764
767
|
|
|
@@ -918,7 +921,7 @@ When your branch and main each generated the same number, merging or rebasing st
|
|
|
918
921
|
|
|
919
922
|
- For non-interactive environments, pass `--yes` to `migration generate` and `--yes` to `apply`. `apply` runs migrations automatically when the `migrations/` directory is configured.
|
|
920
923
|
- Run `tailor tailordb migration validate` in CI to catch uncommitted migrations, broken migration files, unreviewed generated normalization logic, and remote schema drift before deploying. It exits with a non-zero code when validation fails and supports `--json`. Add `--strict` to also require an explicit acknowledgment (a `migrate.ts` or a recorded `--no-script` reason) for every pending migration that can drop data, so destructive changes cannot merge unnoticed.
|
|
921
|
-
- `tailor tailordb migration status` validates the layout and file-format compatibility of the full local history, compares its history ID with the deployed namespace, and shows applied and pending migrations for a human-readable comparison. Its exit code is non-zero on invalid or incompatible files, migration history mismatches, and remote read errors, so check the output.
|
|
924
|
+
- `tailor tailordb migration status` validates the layout and file-format compatibility of the full local history, compares its history ID with the deployed namespace, and shows applied and pending migrations for a human-readable comparison. Its exit code is non-zero on invalid or incompatible files, migration history mismatches, a remote checkpoint ahead of the latest local migration, and remote read errors, so check the output.
|
|
922
925
|
- Avoid running migrations in parallel against the same workspace — there is no locking. Serialize deploys per environment.
|
|
923
926
|
|
|
924
927
|
### Resetting a deployed project
|
|
@@ -1061,7 +1064,7 @@ npm install -D @electric-sql/pglite
|
|
|
1061
1064
|
|
|
1062
1065
|
The generated `db.pglite.ts` exports the `CREATE TABLE` script for the same schema `db.ts` types — the tables as the pre-migration phase leaves them while `migrate.ts` runs, including relaxed constraints, renamed fields under both names, and retained removed fields. Run it once on the PGlite instance, stage rows, then run the script in a transaction. `tailor tailordb migration script <N> --with-test` scaffolds this test too when `@electric-sql/pglite` is installed. Type the instance with `Unmigrated<Database>` rather than `Database`: `db.ts` types a column the migration makes required as `T | null` on read but `T` on write (and an enum it narrows as the old values on read but the new ones on write), so that `migrate.ts` cannot write what the migration is removing — which would also stop the test from staging the rows the script has to convert. `Unmigrated` lets every column be written with whatever it can still be read as; `main` still receives a `Transaction<Database>`.
|
|
1063
1066
|
|
|
1064
|
-
Relations are enforced as on the platform: a row that points at a parent that does not exist is rejected. Write the records the test starts from inside `prepareFixtures`, which leaves relations unchecked while it runs, so you can stage rows that predate a relation, such as rows whose parent does not exist that the script repairs. Relations are checked again when it returns, so the script itself still cannot write a missing parent. The same holds in reverse: the script cannot delete a parent that a row still points at, or change the key a row points at, so delete rows in the opposite order of staging them. NOT NULL, UNIQUE, and CHECK constraints stay enforced. A relation is left unenforced while the pre-migration schema cannot satisfy it: when a pending type change leaves the field a different type from its target, when the migration
|
|
1067
|
+
Relations are enforced as on the platform: a row that points at a parent that does not exist is rejected. Write the records the test starts from inside `prepareFixtures`, which leaves relations unchecked while it runs, so you can stage rows that predate a relation, such as rows whose parent does not exist that the script repairs. Relations are checked again when it returns, so the script itself still cannot write a missing parent. The same holds in reverse: the script cannot delete a parent that a row still points at, or change the key a row points at, so delete rows in the opposite order of staging them. NOT NULL, UNIQUE, and CHECK constraints stay enforced. A relation is left unenforced while the pre-migration schema cannot satisfy it: when a pending type change leaves the field a different type from its target, when the migration adds the relation to an existing top-level field or retargets it to another table, or when the target field's unique constraint is still withheld. How relations on array fields and inside nested objects behave is described under [Real SQL execution with PGlite](../../testing.md#real-sql-execution-with-pglite-mocktailordbwithpglite).
|
|
1065
1068
|
|
|
1066
1069
|
```typescript
|
|
1067
1070
|
// migrations/0005/migrate.pglite.test.ts
|
|
@@ -1108,6 +1111,8 @@ describe("0005 add required email", () => {
|
|
|
1108
1111
|
});
|
|
1109
1112
|
```
|
|
1110
1113
|
|
|
1114
|
+
Queries on `db` wait while the transaction is open, so check the results after `transaction().execute()` returns, or query through `trx` inside it — a query on `db` from inside the transaction waits on itself and never finishes.
|
|
1115
|
+
|
|
1111
1116
|
Pass nested field values as JavaScript objects or arrays of objects, without `JSON.stringify`.
|
|
1112
1117
|
|
|
1113
1118
|
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`.
|
|
@@ -1161,6 +1166,12 @@ For genuinely different schemas across environments, prefer separate workspaces
|
|
|
1161
1166
|
|
|
1162
1167
|
## Troubleshooting
|
|
1163
1168
|
|
|
1169
|
+
### `MIGRATION_DIRECTORY_CONFLICT` error
|
|
1170
|
+
|
|
1171
|
+
**Cause:** Two or more TailorDB namespaces set `migration.directory` to the same directory (after resolving it against the config file's directory). For example, `tailordb` and `analyticsdb` both set `"./migrations"`. Both namespaces would read and write the same `0000/schema.json`, so `migration generate` could not tell them apart and `migration generate --init` would rebuild the history of both. Every `tailor tailordb migration` command and `tailor deploy` fail with this error before touching any migration files. The error lists every shared directory with the namespaces that use it.
|
|
1172
|
+
|
|
1173
|
+
**Resolution:** Give each namespace its own directory in `tailor.config.ts`, for example `"./migrations/tailordb"` and `"./migrations/analyticsdb"`.
|
|
1174
|
+
|
|
1164
1175
|
### Remote schema drift detected
|
|
1165
1176
|
|
|
1166
1177
|
**Cause:** Remote schema doesn't match what the migration history says it should be.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tailor-platform/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.36.0",
|
|
4
4
|
"description": "Tailor Platform SDK - The SDK to work with Tailor Platform",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -164,14 +164,14 @@
|
|
|
164
164
|
"@inquirer/prompts": "8.7.4",
|
|
165
165
|
"@jridgewell/trace-mapping": "0.3.31",
|
|
166
166
|
"@napi-rs/keyring": "2.1.2",
|
|
167
|
-
"@opentelemetry/api": "1.
|
|
168
|
-
"@opentelemetry/context-async-hooks": "
|
|
169
|
-
"@opentelemetry/exporter-trace-otlp-proto": "0.
|
|
170
|
-
"@opentelemetry/resources": "
|
|
171
|
-
"@opentelemetry/sdk-trace": "
|
|
167
|
+
"@opentelemetry/api": "1.10.0",
|
|
168
|
+
"@opentelemetry/context-async-hooks": "3.0.0",
|
|
169
|
+
"@opentelemetry/exporter-trace-otlp-proto": "0.300.0",
|
|
170
|
+
"@opentelemetry/resources": "3.0.0",
|
|
171
|
+
"@opentelemetry/sdk-trace": "3.0.0",
|
|
172
172
|
"@opentelemetry/semantic-conventions": "1.43.0",
|
|
173
173
|
"@oxc-project/types": "0.155.0",
|
|
174
|
-
"@politty/zod": "0.6.
|
|
174
|
+
"@politty/zod": "0.6.3",
|
|
175
175
|
"@secretlint/core": "13.0.7",
|
|
176
176
|
"@secretlint/secretlint-rule-preset-recommend": "13.0.7",
|
|
177
177
|
"@standard-schema/spec": "1.1.0",
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
"amaro": "1.2.2",
|
|
182
182
|
"confbox": "0.3.1",
|
|
183
183
|
"date-fns": "4.4.0",
|
|
184
|
-
"es-toolkit": "1.
|
|
184
|
+
"es-toolkit": "1.53.0",
|
|
185
185
|
"find-up-simple": "1.0.1",
|
|
186
186
|
"get-east-asian-width": "1.7.0",
|
|
187
187
|
"get-tsconfig": "4.14.3",
|
|
@@ -217,12 +217,12 @@
|
|
|
217
217
|
"@types/node": "24.19.2",
|
|
218
218
|
"@types/semver": "7.8.0",
|
|
219
219
|
"@vitest/coverage-v8": "5.0.5",
|
|
220
|
-
"eslint-plugin-zod": "5.2.
|
|
220
|
+
"eslint-plugin-zod": "5.2.2",
|
|
221
221
|
"oxfmt": "0.74.0",
|
|
222
222
|
"oxlint": "1.89.0",
|
|
223
223
|
"oxlint-tsgolint": "7.0.2005",
|
|
224
224
|
"sonda": "0.14.0",
|
|
225
|
-
"tsdown": "0.23.
|
|
225
|
+
"tsdown": "0.23.2",
|
|
226
226
|
"tsrs": "0.16.0-ts7.1.0-dev.20260929",
|
|
227
227
|
"tsx": "4.23.15",
|
|
228
228
|
"typescript": "6.0.3",
|