@tailor-platform/sdk 2.7.0 → 2.10.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 (44) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/{application-D5lh2-_W.mjs → application-D2E3FDfF.mjs} +2 -2
  3. package/dist/application-D2E3FDfF.mjs.map +1 -0
  4. package/dist/application-rySTKrFa.mjs +1 -0
  5. package/dist/cli/lib.mjs +1 -1
  6. package/dist/cli/main.mjs +45 -45
  7. package/dist/cli/main.mjs.map +1 -1
  8. package/dist/cli/shared/seed-context.d.mts +3 -1
  9. package/dist/completion/zsh-worker.zsh +1 -1
  10. package/dist/configure/config/index.d.mts +2 -1
  11. package/dist/configure/index.d.mts +14 -14
  12. package/dist/configure/index.mjs +1 -1
  13. package/dist/configure/index.mjs.map +1 -1
  14. package/dist/configure/services/aigateway/index.d.mts +1 -3
  15. package/dist/configure/services/idp/index.d.mts +1 -1
  16. package/dist/configure/services/resolver/resolver.d.mts +1 -1
  17. package/dist/configure/services/tailordb/schema.d.mts +22 -13
  18. package/dist/configure/types/index.d.mts +1 -1
  19. package/dist/plugin/builtin/seed/index.mjs +1 -1
  20. package/dist/plugin/types.d.mts +17 -7
  21. package/dist/register-ts-hook-D6aNriu3.mjs +642 -0
  22. package/dist/register-ts-hook-D6aNriu3.mjs.map +1 -0
  23. package/dist/{schema-AYG4OhXY.mjs → schema-6d_OHyZf.mjs} +2 -2
  24. package/dist/schema-6d_OHyZf.mjs.map +1 -0
  25. package/dist/{seed-DwqRFdqP.mjs → seed-CMkupmX8.mjs} +41 -17
  26. package/dist/seed-CMkupmX8.mjs.map +1 -0
  27. package/dist/types/helpers.d.mts +3 -1
  28. package/dist/vitest/index.d.mts +2 -2
  29. package/dist/vitest/index.mjs.map +1 -1
  30. package/dist/vitest/pglite-kysely.d.mts +29 -7
  31. package/docs/plugin/custom.md +84 -0
  32. package/docs/services/executor.md +17 -0
  33. package/docs/services/idp.md +1 -1
  34. package/docs/services/resolver.md +1 -1
  35. package/docs/services/tailordb-migration.md +14 -8
  36. package/docs/services/tailordb.md +4 -0
  37. package/docs/services/workflow.md +1 -1
  38. package/package.json +13 -13
  39. package/dist/application-D5lh2-_W.mjs.map +0 -1
  40. package/dist/application-o09L68wE.mjs +0 -1
  41. package/dist/register-ts-hook-CL3Z3VP0.mjs +0 -642
  42. package/dist/register-ts-hook-CL3Z3VP0.mjs.map +0 -1
  43. package/dist/schema-AYG4OhXY.mjs.map +0 -1
  44. package/dist/seed-DwqRFdqP.mjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { Kysely } from "kysely";
1
+ import { ColumnType, Kysely } from "kysely";
2
2
  //#region src/vitest/pglite-kysely.d.ts
3
3
  /**
4
4
  * The subset of a `@electric-sql/pglite` `PGlite` instance used by
@@ -14,11 +14,33 @@ interface PGliteClient {
14
14
  /** Release the underlying database. Called by `db.destroy()`. */
15
15
  close(): Promise<void>;
16
16
  }
17
+ type WritableAs<S, W> = [S] extends [W] ? W : [W] extends [S] ? S : W | Exclude<S, W>;
18
+ type UnmigratedColumn<C> = C extends ColumnType<infer S, infer I, infer U> ? ColumnType<S, WritableAs<S, I>, WritableAs<S, U>> : C;
19
+ /**
20
+ * `DB` as its rows stand before the migration script has run: every column
21
+ * accepts on insert and update whatever it can still hold on read.
22
+ *
23
+ * The generated `db.ts` types a column the migration makes required as
24
+ * `ColumnType<T | null, T, T>`, and an enum whose values it narrows as
25
+ * `ColumnType<Before, After, After>`, so `migrate.ts` cannot write a null
26
+ * or a removed value into them — and neither can a test that has to stage
27
+ * the rows the script converts. Type the PGlite instance with
28
+ * `Unmigrated<Database>` to stage them; `main` still receives a
29
+ * `Transaction<Database>`.
30
+ * @example
31
+ * ```typescript
32
+ * const db = createKyselyPGlite<Unmigrated<Database>>(new PGlite());
33
+ * await db.insertInto("User").values({ name: "a", email: null }).execute();
34
+ * await db.transaction().execute((trx) => main(trx));
35
+ * ```
36
+ */
37
+ type Unmigrated<DB> = { [T in keyof DB]: { [C in keyof DB[T]]: UnmigratedColumn<DB[T][C]>; }; };
17
38
  /**
18
39
  * Create a Kysely instance backed by a PGlite in-memory Postgres, for
19
40
  * executing a migration script's queries against real data in tests.
20
- * Pass the migration's schema as the type argument, e.g.
21
- * `createKyselyPGlite<Database>(new PGlite())`.
41
+ * Pass the migration's schema as the type argument — wrapped in
42
+ * {@link Unmigrated} so the test can stage the rows the script has not yet
43
+ * backfilled: `createKyselyPGlite<Unmigrated<Database>>(new PGlite())`.
22
44
  *
23
45
  * PGlite runs full PostgreSQL while TailorDB supports a subset of it, so a
24
46
  * statement passing here can still be rejected by the platform; keep a
@@ -27,17 +49,17 @@ interface PGliteClient {
27
49
  * @returns A Kysely instance that executes queries on the client and closes it on `destroy()`
28
50
  * @example
29
51
  * ```typescript
30
- * // migrations/0005/migrate.test.ts
52
+ * // migrations/0005/migrate.pglite.test.ts
31
53
  * import { PGlite } from "@electric-sql/pglite";
32
- * import { createKyselyPGlite } from "@tailor-platform/sdk/vitest";
54
+ * import { createKyselyPGlite, type Unmigrated } from "@tailor-platform/sdk/vitest";
33
55
  * import type { Database } from "./db";
34
56
  * import { main } from "./migrate";
35
57
  *
36
- * const db = createKyselyPGlite<Database>(new PGlite());
58
+ * const db = createKyselyPGlite<Unmigrated<Database>>(new PGlite());
37
59
  * // create tables matching db.ts, insert rows, then:
38
60
  * await db.transaction().execute((trx) => main(trx));
39
61
  * ```
40
62
  */
41
63
  declare function createKyselyPGlite<DB = Record<string, never>>(client: PGliteClient): Kysely<DB>;
42
64
  //#endregion
43
- export { PGliteClient, createKyselyPGlite };
65
+ export { PGliteClient, Unmigrated, createKyselyPGlite };
@@ -575,6 +575,90 @@ declare module "@tailor-platform/sdk" {
575
575
  }
576
576
  ```
577
577
 
578
+ ### Injecting fields into the attached table's type (declaration merging)
579
+
580
+ A plugin that adds fields to a table via `onTableLoaded`'s `extends.fields` (see
581
+ [onTableLoaded](#ontableloaded) above) can also make those fields show up on the table's own
582
+ static type right away, computed from the literal per-table config passed to `.plugin()`. Provide
583
+ a declaration merge for the `PluginFieldExtensions` interface, keyed by the same `id` used in
584
+ `PluginConfigs`:
585
+
586
+ ```typescript
587
+ // your-plugin/types.d.ts (shipped with your plugin package)
588
+ import type { TailorDBField } from "@tailor-platform/sdk";
589
+
590
+ declare module "@tailor-platform/sdk" {
591
+ interface PluginConfigs<Fields extends string> {
592
+ "@example/lifecycle": {
593
+ transitions: Record<string, { from: readonly string[]; to: string }>;
594
+ };
595
+ }
596
+
597
+ interface PluginFieldExtensions<Fields extends string, Config> {
598
+ "@example/lifecycle": Config extends {
599
+ transitions: infer T extends Record<string, { from: readonly string[]; to: string }>;
600
+ }
601
+ ? {
602
+ status: TailorDBField<
603
+ { type: "enum"; array: false },
604
+ T[keyof T]["from"][number] | T[keyof T]["to"]
605
+ >;
606
+ }
607
+ : never;
608
+ }
609
+ }
610
+ ```
611
+
612
+ With this in place, the table returned by `.plugin()` already has the derived field. The status enum
613
+ includes every state that appears anywhere in `transitions` — both a `from` state that a transition
614
+ never produces (like the initial `"PENDING"` below) and every `to` state:
615
+
616
+ ```typescript
617
+ import { db } from "@tailor-platform/sdk";
618
+
619
+ const approvalRequest = db.table("ApprovalRequest", { title: db.string() }).plugin({
620
+ "@example/lifecycle": {
621
+ transitions: {
622
+ approve: { from: ["PENDING"], to: "APPROVED" },
623
+ reject: { from: ["PENDING"], to: "REJECTED" },
624
+ },
625
+ },
626
+ });
627
+ // approvalRequest's type now includes status: "PENDING" | "APPROVED" | "REJECTED"
628
+ ```
629
+
630
+ The type registered in `PluginFieldExtensions` must describe only the fields being added — it is
631
+ merged into the table's existing fields, not a replacement for them. A field name that collides
632
+ with an existing field, with a file key declared via `.files()`, or with a field injected by
633
+ another plugin attached in the same `.plugin()` call, is a type error at the call site — regardless
634
+ of whether `.files()` or `.plugin()` was called first. `tailor generate` also rejects the same
635
+ collision at runtime, as a backstop for any case a table's static type doesn't otherwise catch.
636
+
637
+ This only affects the table's static type. The corresponding field exists on the table's
638
+ generated schema, and on the table object's own `fields`, only after `tailor generate` actually
639
+ applies `extends.fields`. Before that, reading an injected field directly off the table
640
+ (`table.fields.status`) returns `undefined`, and `pickFields(["status"])` throws — call these only
641
+ with the table's originally declared fields, not ones a plugin injects.
642
+
643
+ To keep the declared type and the runtime implementation in sync, give `Plugin`'s optional third
644
+ type parameter the same shape and use it inside `onTableLoaded`:
645
+
646
+ ```typescript
647
+ import { db, type Plugin, type TailorDBField } from "@tailor-platform/sdk";
648
+
649
+ const lifecyclePlugin: Plugin<
650
+ LifecycleTableConfig,
651
+ LifecyclePluginConfig,
652
+ { status: TailorDBField<{ type: "enum"; array: false }, "PENDING" | "APPROVED" | "REJECTED"> }
653
+ > = {
654
+ id: "@example/lifecycle",
655
+ description: "Derives a status field from a transitions map",
656
+ onTableLoaded(context) {
657
+ return { extends: { fields: { status: db.enum(["PENDING", "APPROVED", "REJECTED"]) } } };
658
+ },
659
+ };
660
+ ```
661
+
578
662
  ### Resolving plugin-level config from a `Plugin[]` array (declaration merging)
579
663
 
580
664
  `PluginConfig` is already available inside your own plugin's hooks via `context.pluginConfig`.
@@ -44,6 +44,23 @@ export default createExecutor({
44
44
  });
45
45
  ```
46
46
 
47
+ ### Disabling an Executor
48
+
49
+ Set `disabled: true` to deploy an executor without letting it run. The definition stays deployed, and the platform skips dispatching jobs to it.
50
+
51
+ ```typescript
52
+ export default createExecutor({
53
+ name: "user-welcome-paused",
54
+ disabled: true,
55
+ trigger: recordCreatedTrigger({ type: user }),
56
+ operation: { kind: "function", body: async () => {} },
57
+ });
58
+ ```
59
+
60
+ A disabled executor does not count as a subscriber when `deploy` resolves `publishEvents` on the resource its trigger names. Publishing is not enabled on that resource's behalf, and an explicit `publishEvents: false` on it is not rejected. If the resource was publishing only for this executor, the next `deploy` turns publishing back off — so disabling the last subscriber of a shared table changes that table for every config that reads its events.
61
+
62
+ The trigger is still deployed, so it must still name a resource the deploy declares; a disabled executor whose trigger names nothing fails the deploy exactly as an enabled one does.
63
+
47
64
  ## Trigger Types
48
65
 
49
66
  ### Record Triggers
@@ -330,7 +330,7 @@ defineIdp("my-idp", {
330
330
  });
331
331
  ```
332
332
 
333
- **Auto-configuration:** When `publishEvents` is omitted, `deploy` sets it from the executors taking part in the same run: `true` while one of their `idpUser` triggers targets this IdP, and `false` once none does. Removing the last such trigger turns publishing back off on the next `deploy`. Targeting is per-IdP: an executor specifies which IdP it subscribes to via the trigger's `idp` option (required in multi-IdP projects). Set the value explicitly to override:
333
+ **Auto-configuration:** When `publishEvents` is omitted, `deploy` sets it from the executors taking part in the same run: `true` while one of their `idpUser` triggers targets this IdP, and `false` once none does. Removing the last such trigger turns publishing back off on the next `deploy`, as does disabling the last executor that carries one. Targeting is per-IdP: an executor specifies which IdP it subscribes to via the trigger's `idp` option (required in multi-IdP projects). Set the value explicitly to override:
334
334
 
335
335
  - `publishEvents: true`: always publish events.
336
336
  - `publishEvents: false`: never publish events. `deploy` rejects this with an error if an `idpUser` trigger taking part in the same run targets this IdP — either remove `publishEvents: false` or remove the matching trigger.
@@ -310,7 +310,7 @@ createResolver({
310
310
 
311
311
  **Use cases:**
312
312
 
313
- 1. **Auto-detection (recommended)**: Don't set `publishEvents` - `deploy` enables it while an executor taking part in the same run needs it
313
+ 1. **Auto-detection (recommended)**: Don't set `publishEvents` - `deploy` enables it while an executor taking part in the same run needs it. An executor declared with `disabled: true` never runs, so it does not count
314
314
 
315
315
  ```typescript
316
316
  // publishEvents is automatically enabled because an executor uses this resolver
@@ -815,25 +815,27 @@ A statement-level test verifies what the script issues, not what it does to data
815
815
  npm install -D @electric-sql/pglite
816
816
  ```
817
817
 
818
- Create the tables the script touches (matching the shape in the generated `db.ts`), stage rows, then run the script in a transaction:
818
+ Create the tables the script touches (matching the shape in the generated `db.ts`), stage rows, then run the script in a transaction. 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>`.
819
819
 
820
820
  ```typescript
821
821
  // migrations/0005/migrate.pglite.test.ts
822
822
  import { PGlite } from "@electric-sql/pglite";
823
823
  import { sql } from "@tailor-platform/sdk/kysely";
824
- import { createKyselyPGlite } from "@tailor-platform/sdk/vitest";
824
+ import { createKyselyPGlite, type Unmigrated } from "@tailor-platform/sdk/vitest";
825
825
  import { afterAll, beforeAll, describe, expect, test } from "vitest";
826
826
  import type { Database } from "./db";
827
827
  import { main } from "./migrate";
828
828
 
829
- const db = createKyselyPGlite<Database>(new PGlite());
829
+ const db = createKyselyPGlite<Unmigrated<Database>>(new PGlite());
830
830
 
831
831
  beforeAll(async () => {
832
832
  await sql`
833
833
  CREATE TABLE "User" (
834
834
  "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
835
835
  "name" text NOT NULL,
836
- "email" text
836
+ "email" text,
837
+ "createdAt" timestamptz NOT NULL,
838
+ "updatedAt" timestamptz NOT NULL
837
839
  )
838
840
  `.execute(db);
839
841
  });
@@ -844,10 +846,14 @@ afterAll(async () => {
844
846
 
845
847
  describe("0005 add required email", () => {
846
848
  test("backfills null emails and keeps existing ones", async () => {
847
- await sql`
848
- INSERT INTO "User" ("name", "email")
849
- VALUES ('a', NULL), ('b', 'b@example.com')
850
- `.execute(db);
849
+ const now = new Date();
850
+ await db
851
+ .insertInto("User")
852
+ .values([
853
+ { name: "a", email: null, createdAt: now, updatedAt: now },
854
+ { name: "b", email: "b@example.com", createdAt: now, updatedAt: now },
855
+ ])
856
+ .execute();
851
857
 
852
858
  await db.transaction().execute((trx) => main(trx));
853
859
 
@@ -519,6 +519,8 @@ db.table("User", {
519
519
  - When `publishEvents: true`, record creation/update/deletion events are published
520
520
  - 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`
521
521
  - When explicitly set to `false` while an executor taking part in the same run uses this table, `deploy` fails
522
+ - An executor declared with `disabled: true` never runs, so it does not count as using the table
523
+ - While a `deploy` applies pending migrations, every table in the migrating namespace is read-only: create, update, delete, and bulk-upsert operations are disabled while the existing read setting is preserved. Record event publishing is also switched off so a migration script's writes do not reach executors from the previous deploy. After success, the configured settings take effect. If a later migration fails, settings from the last confirmed checkpoint are restored; an uncommitted migration restores existing tables' prior settings and leaves its newly created tables restricted until a successful retry. Restoration is skipped if the checkpoint number or migration history changed concurrently, or if checkpoint ownership cannot be verified. A table left behind by a failed post-checkpoint deletion also remains read-only for manual recovery
522
524
 
523
525
  **Use cases:**
524
526
 
@@ -563,6 +565,8 @@ db.table("User", {
563
565
 
564
566
  Control which GraphQL operations (`create`, `update`, `delete`, `read`) are exposed for a table. All operations are enabled by default.
565
567
 
568
+ While a `deploy` applies pending migrations, every GraphQL operation — `create`, `update`, `delete`, `read`, and bulk upsert — is switched off across the migrating namespace, so nothing reads or writes an intermediate schema. After success, the configured operations take effect. If a later migration fails, operations from the last confirmed checkpoint are restored; an uncommitted migration restores existing tables' prior operations and leaves its newly created tables restricted until a successful retry. Restoration is skipped if the checkpoint number or migration history changed concurrently.
569
+
566
570
  ```typescript
567
571
  db.table("Order", {
568
572
  status: db.string(),
@@ -208,7 +208,7 @@ export default createWorkflow({
208
208
 
209
209
  Workflows can publish execution lifecycle events for executors. When an executor subscribes to a workflow's events, `deploy` enables publishing automatically. A `workflowExecution*` trigger enables it on the workflow, and a `workflowJobExecution*` trigger enables it on every job that workflow runs. See [Workflow Execution Triggers](./executor.md#workflow-execution-triggers).
210
210
 
211
- Publishing follows the subscription in both directions: removing the last subscribing trigger turns it back off on the next `deploy`.
211
+ Publishing follows the subscription in both directions: removing the last subscribing trigger turns it back off on the next `deploy`, as does disabling the last executor that carries one.
212
212
 
213
213
  Set `publishEvents` explicitly to pin the value instead. Use `true` to publish workflow-level events with no subscribing executor:
214
214
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tailor-platform/sdk",
3
- "version": "2.7.0",
3
+ "version": "2.10.0",
4
4
  "description": "Tailor Platform SDK - The SDK to work with Tailor Platform",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -155,8 +155,8 @@
155
155
  "@bufbuild/protovalidate": "1.2.0",
156
156
  "@connectrpc/connect": "2.1.2",
157
157
  "@connectrpc/connect-node": "2.1.2",
158
- "@inquirer/core": "12.0.0",
159
- "@inquirer/prompts": "8.6.0",
158
+ "@inquirer/core": "12.0.1",
159
+ "@inquirer/prompts": "8.7.0",
160
160
  "@jridgewell/trace-mapping": "0.3.31",
161
161
  "@napi-rs/keyring": "1.3.0",
162
162
  "@opentelemetry/api": "1.9.1",
@@ -164,9 +164,9 @@
164
164
  "@opentelemetry/resources": "2.10.0",
165
165
  "@opentelemetry/sdk-trace-node": "2.10.0",
166
166
  "@opentelemetry/semantic-conventions": "1.43.0",
167
- "@oxc-project/types": "0.146.0",
168
- "@secretlint/core": "13.0.4",
169
- "@secretlint/secretlint-rule-preset-recommend": "13.0.4",
167
+ "@oxc-project/types": "0.147.0",
168
+ "@secretlint/core": "13.0.5",
169
+ "@secretlint/secretlint-rule-preset-recommend": "13.0.5",
170
170
  "@standard-schema/spec": "1.1.0",
171
171
  "@tailor-platform/function-kysely-tailordb": "0.1.3",
172
172
  "@toiroakr/lines-db": "0.12.2",
@@ -185,7 +185,7 @@
185
185
  "kysely": "0.29.5",
186
186
  "mime-types": "3.0.2",
187
187
  "open": "11.0.1",
188
- "oxc-parser": "0.146.0",
188
+ "oxc-parser": "0.147.0",
189
189
  "p-limit": "7.3.1",
190
190
  "pathe": "2.0.3",
191
191
  "pgsql-ast-parser": "12.0.2",
@@ -202,22 +202,22 @@
202
202
  },
203
203
  "devDependencies": {
204
204
  "@opentelemetry/sdk-trace-base": "2.10.0",
205
+ "@tailor-platform/shared": "^0.0.0",
206
+ "@tailor-platform/tailor-proto": "^0.0.1",
205
207
  "@types/mime-types": "3.0.1",
206
208
  "@types/node": "24.13.3",
207
209
  "@types/semver": "7.8.0",
208
210
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
209
211
  "@vitest/coverage-v8": "4.1.11",
210
- "eslint-plugin-zod": "4.9.1",
211
- "oxfmt": "0.64.0",
212
- "oxlint": "1.79.0",
212
+ "eslint-plugin-zod": "4.10.0",
213
+ "oxfmt": "0.65.0",
214
+ "oxlint": "1.80.0",
213
215
  "oxlint-tsgolint": "7.0.2001",
214
216
  "sonda": "0.14.0",
215
217
  "tsdown": "0.22.14",
216
218
  "typescript": "6.0.3",
217
219
  "vitest": "4.1.11",
218
- "zinfer": "0.2.8",
219
- "@tailor-platform/shared": "^0.0.0",
220
- "@tailor-platform/tailor-proto": "^0.0.1"
220
+ "zinfer": "0.2.8"
221
221
  },
222
222
  "peerDependencies": {
223
223
  "@electric-sql/pglite": ">=0.2.0",