@tailor-platform/sdk 2.8.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 (33) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/cli/lib.mjs +1 -1
  3. package/dist/cli/main.mjs +1 -1
  4. package/dist/cli/shared/seed-context.d.mts +3 -1
  5. package/dist/completion/zsh-worker.zsh +1 -1
  6. package/dist/configure/config/index.d.mts +2 -1
  7. package/dist/configure/index.d.mts +14 -14
  8. package/dist/configure/index.mjs +1 -1
  9. package/dist/configure/index.mjs.map +1 -1
  10. package/dist/configure/services/aigateway/index.d.mts +1 -3
  11. package/dist/configure/services/idp/index.d.mts +1 -1
  12. package/dist/configure/services/resolver/resolver.d.mts +1 -1
  13. package/dist/configure/services/tailordb/schema.d.mts +22 -13
  14. package/dist/configure/types/index.d.mts +1 -1
  15. package/dist/plugin/builtin/seed/index.mjs +1 -1
  16. package/dist/plugin/types.d.mts +17 -7
  17. package/dist/{register-ts-hook-CQuJ7h5G.mjs → register-ts-hook-D6aNriu3.mjs} +39 -39
  18. package/dist/register-ts-hook-D6aNriu3.mjs.map +1 -0
  19. package/dist/{schema-AYG4OhXY.mjs → schema-6d_OHyZf.mjs} +2 -2
  20. package/dist/schema-6d_OHyZf.mjs.map +1 -0
  21. package/dist/{seed-DwqRFdqP.mjs → seed-CMkupmX8.mjs} +41 -17
  22. package/dist/seed-CMkupmX8.mjs.map +1 -0
  23. package/dist/types/helpers.d.mts +3 -1
  24. package/dist/vitest/index.d.mts +2 -2
  25. package/dist/vitest/index.mjs.map +1 -1
  26. package/dist/vitest/mocks/file.d.mts +1 -1
  27. package/dist/vitest/pglite-kysely.d.mts +29 -7
  28. package/docs/plugin/custom.md +84 -0
  29. package/docs/services/tailordb-migration.md +14 -8
  30. package/package.json +5 -5
  31. package/dist/register-ts-hook-CQuJ7h5G.mjs.map +0 -1
  32. package/dist/schema-AYG4OhXY.mjs.map +0 -1
  33. package/dist/seed-DwqRFdqP.mjs.map +0 -1
@@ -46,10 +46,10 @@ declare function mockFile(options?: MockFileOptions): {
46
46
  calls: FileCall[];
47
47
  clear(): void;
48
48
  reset(): void;
49
+ delete: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<void>>;
49
50
  upload: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string, data: string | ArrayBuffer | Uint8Array | number[], options?: FileUploadOptions) => Promise<FileUploadResponse>>;
50
51
  download: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<FileDownloadResponse>>;
51
52
  downloadAsBase64: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<FileDownloadAsBase64Response>>;
52
- delete: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<void>>;
53
53
  getMetadata: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<FileMetadata>>;
54
54
  downloadStream: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string) => Promise<FileDownloadStreamResponse>>;
55
55
  uploadStream: Mock<(namespace: string, tableName: string, fieldName: string, recordId: string, readableStream: ReadableStream<Uint8Array | ArrayBuffer>, options?: FileUploadStreamOptions) => Promise<FileUploadResponse>>;
@@ -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`.
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tailor-platform/sdk",
3
- "version": "2.8.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",
@@ -165,8 +165,8 @@
165
165
  "@opentelemetry/sdk-trace-node": "2.10.0",
166
166
  "@opentelemetry/semantic-conventions": "1.43.0",
167
167
  "@oxc-project/types": "0.147.0",
168
- "@secretlint/core": "13.0.4",
169
- "@secretlint/secretlint-rule-preset-recommend": "13.0.4",
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",