From c62c3b0a78761cff859ccac5af23d2e8dd0f996a Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 7 May 2026 15:09:24 +0900 Subject: [PATCH 01/35] feat(sdk): vendor function-types and add @tailor-platform/sdk/runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Internalize the external @tailor-platform/function-types package into the SDK and expose it as a new `@tailor-platform/sdk/runtime` entry. The runtime entry provides typed wrappers (iconv, secretmanager, authconnection, idp, workflow, context, file) for the platform's `tailor.*` and `tailordb.*` globals, and importing it activates the corresponding ambient global types as a side effect. Importing from `@tailor-platform/sdk` also activates those globals automatically — existing projects do not need to add anything to tsconfig. Co-Authored-By: Claude Opus 4.7 --- .changeset/runtime-wrapper.md | 16 + packages/sdk/README.md | 1 + packages/sdk/docs/runtime.md | 163 ++++++ packages/sdk/package.json | 11 +- packages/sdk/skills/tailor-sdk/SKILL.md | 2 + packages/sdk/src/configure/index.ts | 11 +- .../runtime/__tests__/authconnection.test.ts | 35 ++ .../sdk/src/runtime/__tests__/context.test.ts | 24 + .../sdk/src/runtime/__tests__/file.test.ts | 125 ++++ .../sdk/src/runtime/__tests__/globals.test.ts | 41 ++ .../sdk/src/runtime/__tests__/iconv.test.ts | 105 ++++ .../sdk/src/runtime/__tests__/idp.test.ts | 87 +++ .../runtime/__tests__/secretmanager.test.ts | 52 ++ .../src/runtime/__tests__/workflow.test.ts | 73 +++ packages/sdk/src/runtime/authconnection.ts | 23 + packages/sdk/src/runtime/context.ts | 28 + packages/sdk/src/runtime/file.ts | 145 +++++ packages/sdk/src/runtime/globals.ts | 545 ++++++++++++++++++ packages/sdk/src/runtime/iconv.ts | 104 ++++ packages/sdk/src/runtime/idp.ts | 114 ++++ packages/sdk/src/runtime/index.ts | 31 + packages/sdk/src/runtime/secretmanager.ts | 38 ++ packages/sdk/src/runtime/workflow.ts | 76 +++ .../src/vitest/__tests__/mock-types.test.ts | 5 +- packages/sdk/tsconfig.json | 3 +- packages/sdk/tsdown.config.ts | 5 +- pnpm-lock.yaml | 8 - 27 files changed, 1854 insertions(+), 17 deletions(-) create mode 100644 .changeset/runtime-wrapper.md create mode 100644 packages/sdk/docs/runtime.md create mode 100644 packages/sdk/src/runtime/__tests__/authconnection.test.ts create mode 100644 packages/sdk/src/runtime/__tests__/context.test.ts create mode 100644 packages/sdk/src/runtime/__tests__/file.test.ts create mode 100644 packages/sdk/src/runtime/__tests__/globals.test.ts create mode 100644 packages/sdk/src/runtime/__tests__/iconv.test.ts create mode 100644 packages/sdk/src/runtime/__tests__/idp.test.ts create mode 100644 packages/sdk/src/runtime/__tests__/secretmanager.test.ts create mode 100644 packages/sdk/src/runtime/__tests__/workflow.test.ts create mode 100644 packages/sdk/src/runtime/authconnection.ts create mode 100644 packages/sdk/src/runtime/context.ts create mode 100644 packages/sdk/src/runtime/file.ts create mode 100644 packages/sdk/src/runtime/globals.ts create mode 100644 packages/sdk/src/runtime/iconv.ts create mode 100644 packages/sdk/src/runtime/idp.ts create mode 100644 packages/sdk/src/runtime/index.ts create mode 100644 packages/sdk/src/runtime/secretmanager.ts create mode 100644 packages/sdk/src/runtime/workflow.ts diff --git a/.changeset/runtime-wrapper.md b/.changeset/runtime-wrapper.md new file mode 100644 index 000000000..e71b62eb6 --- /dev/null +++ b/.changeset/runtime-wrapper.md @@ -0,0 +1,16 @@ +--- +"@tailor-platform/sdk": minor +--- + +Add `@tailor-platform/sdk/runtime` — typed wrappers for the Tailor Platform Function runtime APIs (`tailor.iconv`, `tailor.secretmanager`, `tailor.authconnection`, `tailor.idp`, `tailor.workflow`, `tailor.context`, and `tailordb.file`). Importing the entry also activates the corresponding ambient `tailor.*` / `tailordb` global types, so existing code that calls `tailor.iconv.convert(...)` directly continues to type-check. + +```ts +import { iconv, secretmanager, idp, file } from "@tailor-platform/sdk/runtime"; + +const utf8 = iconv.convert(sjisBuffer, "Shift_JIS", "UTF-8"); +const apiKey = await secretmanager.getSecret("my-vault", "API_KEY"); +const client = new idp.Client({ namespace: "my-namespace" }); +const { metadata } = await file.upload("ns", "Document", "attachment", recordId, bytes); +``` + +The SDK no longer depends on the external `@tailor-platform/function-types` package; its declarations are now vendored inside the SDK and exported as `@tailor-platform/sdk/runtime/globals` for projects that prefer to pin global types via `tsconfig.json`'s `compilerOptions.types`. Most users do not need to import `/runtime/globals` directly — `@tailor-platform/sdk/runtime` activates the ambient types as a side effect. diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 816587aca..71cb46e9e 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -85,6 +85,7 @@ the installed SDK version. Files are copied (not symlinked) so they survive ### Guides +- [Runtime API](./docs/runtime.md) - Typed wrappers for `tailor.iconv`, `tailor.secretmanager`, `tailor.idp`, `tailor.workflow`, `tailor.context`, `tailor.authconnection`, and `tailordb.file` - [Testing Guide](./docs/testing.md) - Unit and E2E testing patterns - [CLI Reference](./docs/cli-reference.md) - Command-line interface documentation diff --git a/packages/sdk/docs/runtime.md b/packages/sdk/docs/runtime.md new file mode 100644 index 000000000..eb3d84d37 --- /dev/null +++ b/packages/sdk/docs/runtime.md @@ -0,0 +1,163 @@ +# Runtime API + +`@tailor-platform/sdk/runtime` provides typed wrappers for the `tailor.*` and `tailordb.file` APIs that the Tailor Platform Function runtime injects into the global scope at execution time. The wrappers are thin and delegate to the platform-provided globals; they exist so that you can: + +- Reach the runtime API without relying on a separate ambient `.d.ts` package +- Get IDE-friendly imports (`iconv.convert`, `idp.Client`, …) instead of unmemorable `tailor.iconv.convert(...)` calls +- Use the same module surface in resolvers, executors, and workflows + +Importing this module also activates the ambient `tailor.*` / `tailordb` global types as a side effect — code that calls `tailor.iconv.convert(...)` directly continues to type-check. + +## Quick Start + +```ts +import { + iconv, + secretmanager, + authconnection, + idp, + workflow, + context, + file, +} from "@tailor-platform/sdk/runtime"; + +const utf8 = iconv.convert(sjisBuffer, "Shift_JIS", "UTF-8"); + +const apiKey = await secretmanager.getSecret("my-vault", "API_KEY"); + +const token = await authconnection.getConnectionToken("google"); + +const client = new idp.Client({ namespace: "my-namespace" }); +const { users } = await client.users({ first: 10 }); + +const executionId = await workflow.triggerWorkflow("approval", { reportId }); + +const invoker = context.getInvoker(); + +const { metadata } = await file.upload("my-namespace", "Document", "attachment", recordId, bytes); +``` + +## Subpath imports + +Each namespace can also be imported individually so you only pull what you need: + +```ts +import { iconv } from "@tailor-platform/sdk/runtime"; +import type { ListUsersResponse, ClientConfig } from "@tailor-platform/sdk/runtime/idp"; +``` + +> Type-only re-exports follow the platform contract. If a future runtime release adds new fields, the SDK will publish them in lockstep — there is no separate `@tailor-platform/function-types` package to upgrade. + +## Activating the global types + +Most users do not need to touch the globals entry. Importing `@tailor-platform/sdk/runtime` once anywhere in your project is enough — the side-effect import wires up the `declare global { … }` block so that calls like `tailor.iconv.convert(...)` and `new tailordb.Client(...)` type-check from any other file. + +If you prefer to enable the globals without an `import`, register them in `tsconfig.json`: + +```jsonc +{ + "compilerOptions": { + "types": ["@tailor-platform/sdk/runtime/globals"], + }, +} +``` + +## API Reference + +### `iconv` + +Character encoding conversion. The return type narrows to `string` for `"UTF8"` / `"UTF-8"` targets and `Uint8Array` otherwise. + +| Function | Description | +| --------------- | ---------------------------------------------------------- | +| `convert` | Convert a string or buffer between encodings | +| `convertBuffer` | Convert bytes between encodings | +| `decode` | Decode bytes to a UTF-8 string | +| `encode` | Encode a UTF-8 string into the given target encoding | +| `encodings` | List supported encoding names | +| `Iconv` (class) | Stateful converter for repeated conversions (`node-iconv`) | + +### `secretmanager` + +| Function | Returns | +| ------------ | --------------------------------------------- | +| `getSecret` | `Promise` | +| `getSecrets` | `Promise>>` | + +Pass the `names` argument as a `const` tuple to narrow the result keys: `getSecrets("v", ["A", "B"] as const)`. + +### `authconnection` + +| Function | Returns | +| -------------------- | ----------------------- | +| `getConnectionToken` | Provider-specific token | + +### `idp` + +`new idp.Client({ namespace })` exposes the IdP user APIs: + +- `users(options?)`, `user(userId)`, `userByName(name)` +- `createUser(input)`, `updateUser(input)`, `deleteUser(userId)` +- `sendPasswordResetEmail({ userId, redirectUri })` + +### `workflow` + +| Function | Description | +| -------------------- | ------------------------------------------------- | +| `triggerWorkflow` | Trigger a workflow and return its execution ID | +| `triggerJobFunction` | Synchronously trigger a job and return its result | +| `wait` | Suspend a job at a wait point | +| `resolve` | Resolve a wait point on a running execution | + +### `context` + +| Function | Returns | +| ------------ | -------------------------------------- | +| `getInvoker` | `Invoker \| null` (anonymous = `null`) | + +### `file` + +`tailordb.file` BLOB API. The exported `delete` function is renamed from `deleteFile` to avoid the reserved keyword. + +| Function | Description | +| -------------------- | -------------------------------------------------- | +| `upload` | Upload bytes for a record's file field | +| `download` | Download a file (≤ 10 MB) | +| `downloadAsBase64` | Download a file as a Base64 string (≤ 10 MB) | +| `delete` | Delete a file | +| `getMetadata` | Fetch file metadata only | +| `openDownloadStream` | Open an async iterator for files larger than 10 MB | + +For files larger than 10 MB, `download` and `downloadAsBase64` throw `TailorDBFileError` with code `FILE_TOO_LARGE`; switch to `openDownloadStream` for those. + +## Testing + +`@tailor-platform/sdk/vitest` ships mock controllers for every runtime namespace. Pair them with the `tailor-runtime` Vitest environment so your unit tests run against the same wrappers your production code does. + +```ts +import { iconv, secretmanager } from "@tailor-platform/sdk/runtime"; +import { iconvMock, secretmanagerMock } from "@tailor-platform/sdk/vitest"; +import { beforeEach, expect, test } from "vitest"; + +beforeEach(() => { + iconvMock.reset(); + secretmanagerMock.reset(); +}); + +test("encodes via iconv", () => { + iconvMock.setResolver(() => new Uint8Array([0x82, 0xa0])); + + const out = iconv.convert("あ", "UTF-8", "Shift_JIS"); + + expect(out).toEqual(new Uint8Array([0x82, 0xa0])); + expect(iconvMock.calls[0]?.method).toBe("convert"); +}); + +test("reads from a vault", async () => { + secretmanagerMock.setSecrets({ "my-vault": { API_KEY: "sk-123" } }); + + await expect(secretmanager.getSecret("my-vault", "API_KEY")).resolves.toBe("sk-123"); +}); +``` + +See [Testing Guide](./testing.md#runtime-environment-emulation-beta) for the full list of mock controllers and the `tailor-runtime` environment setup. diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 835c5974d..c7d9c6e36 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -83,6 +83,16 @@ "types": "./dist/vitest/environment.d.mts", "import": "./dist/vitest/environment.mjs", "default": "./dist/vitest/environment.mjs" + }, + "./runtime": { + "types": "./dist/runtime/index.d.mts", + "import": "./dist/runtime/index.mjs", + "default": "./dist/runtime/index.mjs" + }, + "./runtime/globals": { + "types": "./dist/runtime/globals.d.mts", + "import": "./dist/runtime/globals.mjs", + "default": "./dist/runtime/globals.mjs" } }, "scripts": { @@ -125,7 +135,6 @@ "@oxc-project/types": "0.127.0", "@standard-schema/spec": "1.1.0", "@tailor-platform/function-kysely-tailordb": "0.1.3", - "@tailor-platform/function-types": "0.8.5", "@toiroakr/lines-db": "0.9.2", "@toiroakr/read-multiline": "0.3.2", "@urql/core": "6.0.1", diff --git a/packages/sdk/skills/tailor-sdk/SKILL.md b/packages/sdk/skills/tailor-sdk/SKILL.md index ee1852d2a..7d1dcdcf2 100644 --- a/packages/sdk/skills/tailor-sdk/SKILL.md +++ b/packages/sdk/skills/tailor-sdk/SKILL.md @@ -17,6 +17,7 @@ Use these files as the single source of truth: - `node_modules/@tailor-platform/sdk/docs/cli-reference.md` - `node_modules/@tailor-platform/sdk/docs/cli/*.md` - `node_modules/@tailor-platform/sdk/docs/testing.md` +- `node_modules/@tailor-platform/sdk/docs/runtime.md` ## Working Rules @@ -32,3 +33,4 @@ Use these files as the single source of truth: - Service details: `docs/services/*.md` - CLI commands: `docs/cli-reference.md` and `docs/cli/*.md` - Testing patterns: `docs/testing.md` +- Runtime API wrappers (`tailor.iconv`, `tailor.secretmanager`, `tailor.idp`, `tailor.workflow`, `tailor.context`, `tailor.authconnection`, `tailordb.file`): `docs/runtime.md` diff --git a/packages/sdk/src/configure/index.ts b/packages/sdk/src/configure/index.ts index 3582d736d..35d345b44 100644 --- a/packages/sdk/src/configure/index.ts +++ b/packages/sdk/src/configure/index.ts @@ -1,7 +1,16 @@ -/// import { t as _t } from "@/configure/types"; import type * as helperTypes from "@/types/helpers"; +/** + * Re-exported so the bundled `.d.mts` keeps a value-level reference to + * `@/runtime/globals`, which forces the rolldown dts emitter to include the + * vendored ambient `tailor.*` / `tailordb` declarations alongside the SDK + * main entry. Importing anything from `@tailor-platform/sdk` therefore + * activates those globals automatically. + * @internal + */ +export { __TAILOR_RUNTIME_GLOBALS_LOADED__ } from "@/runtime/globals"; + type TailorOutput = helperTypes.output; export type infer = TailorOutput; diff --git a/packages/sdk/src/runtime/__tests__/authconnection.test.ts b/packages/sdk/src/runtime/__tests__/authconnection.test.ts new file mode 100644 index 000000000..e2634c7de --- /dev/null +++ b/packages/sdk/src/runtime/__tests__/authconnection.test.ts @@ -0,0 +1,35 @@ +/** + * Tests for `@tailor-platform/sdk/runtime/authconnection` typed wrappers. + */ +import "@/runtime/globals"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import * as authconnection from "@/runtime/authconnection"; +import { authconnectionMock, cleanupMocks, injectMocks } from "@/vitest/mock"; + +describe("@tailor-platform/sdk/runtime/authconnection", () => { + beforeEach(() => { + injectMocks(globalThis); + authconnectionMock.reset(); + }); + + afterEach(() => { + cleanupMocks(globalThis); + }); + + test("getConnectionToken forwards to global and records call", async () => { + authconnectionMock.setTokens({ + google: { access_token: "ya29.xxx", expires_in: 3600 }, + }); + + const result = await authconnection.getConnectionToken("google"); + + expect(result).toEqual({ access_token: "ya29.xxx", expires_in: 3600 }); + expect(authconnectionMock.calls).toEqual([{ connectionName: "google" }]); + }); + + test("returns default token for unknown connection", async () => { + const result = await authconnection.getConnectionToken("unknown"); + + expect(result).toEqual({ access_token: "mock-token" }); + }); +}); diff --git a/packages/sdk/src/runtime/__tests__/context.test.ts b/packages/sdk/src/runtime/__tests__/context.test.ts new file mode 100644 index 000000000..43e145ace --- /dev/null +++ b/packages/sdk/src/runtime/__tests__/context.test.ts @@ -0,0 +1,24 @@ +/** + * Tests for `@tailor-platform/sdk/runtime/context` typed wrappers. + */ +import "@/runtime/globals"; +import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; +import * as context from "@/runtime/context"; +import { cleanupMocks, injectMocks } from "@/vitest/mock"; + +describe("@tailor-platform/sdk/runtime/context", () => { + beforeEach(() => { + injectMocks(globalThis); + }); + + afterEach(() => { + cleanupMocks(globalThis); + }); + + test("getInvoker forwards to global and returns Invoker | null", () => { + const result = context.getInvoker(); + + expectTypeOf(result).toEqualTypeOf(); + expect(result).toBeNull(); + }); +}); diff --git a/packages/sdk/src/runtime/__tests__/file.test.ts b/packages/sdk/src/runtime/__tests__/file.test.ts new file mode 100644 index 000000000..b76a31acb --- /dev/null +++ b/packages/sdk/src/runtime/__tests__/file.test.ts @@ -0,0 +1,125 @@ +/** + * Tests for `@tailor-platform/sdk/runtime/file` typed wrappers. + */ +import "@/runtime/globals"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import * as file from "@/runtime/file"; +import { cleanupMocks, fileMock, injectMocks } from "@/vitest/mock"; + +describe("@tailor-platform/sdk/runtime/file", () => { + beforeEach(() => { + injectMocks(globalThis); + fileMock.reset(); + }); + + afterEach(() => { + cleanupMocks(globalThis); + }); + + test("upload forwards args and records the call", async () => { + fileMock.enqueueResult({ metadata: { fileSize: 4, sha256sum: "abc" } }); + + const result = await file.upload("ns", "Doc", "blob", "rec-1", new Uint8Array([1, 2, 3, 4])); + + expect(result).toEqual({ metadata: { fileSize: 4, sha256sum: "abc" } }); + expect(fileMock.calls).toEqual([ + { + method: "upload", + namespace: "ns", + typeName: "Doc", + fieldName: "blob", + recordId: "rec-1", + }, + ]); + }); + + test("download forwards and returns the queued payload", async () => { + fileMock.enqueueResult({ + data: new Uint8Array([9, 9]), + metadata: { + contentType: "application/octet-stream", + fileSize: 2, + sha256sum: "h", + lastUploadedAt: "2026-01-01T00:00:00Z", + }, + }); + + const result = await file.download("ns", "Doc", "blob", "rec-1"); + + expect(result.data).toEqual(new Uint8Array([9, 9])); + expect(fileMock.calls[0]?.method).toBe("download"); + }); + + test("downloadAsBase64 forwards", async () => { + fileMock.enqueueResult({ + data: "AQID", + metadata: { + contentType: "application/octet-stream", + fileSize: 3, + sha256sum: "h", + lastUploadedAt: "2026-01-01T00:00:00Z", + }, + }); + + const result = await file.downloadAsBase64("ns", "Doc", "blob", "rec-1"); + + expect(result.data).toBe("AQID"); + expect(fileMock.calls[0]?.method).toBe("downloadAsBase64"); + }); + + test("getMetadata forwards", async () => { + fileMock.enqueueResult({ + contentType: "image/png", + fileSize: 100, + sha256sum: "x", + urlPath: "/url", + }); + + const meta = await file.getMetadata("ns", "Doc", "blob", "rec-1"); + + expect(meta.contentType).toBe("image/png"); + expect(fileMock.calls[0]?.method).toBe("getMetadata"); + }); + + test("delete forwards (re-exported from deleteFile)", async () => { + await file.delete("ns", "Doc", "blob", "rec-1"); + + expect(fileMock.calls).toEqual([ + { + method: "delete", + namespace: "ns", + typeName: "Doc", + fieldName: "blob", + recordId: "rec-1", + }, + ]); + }); + + test("openDownloadStream forwards and yields chunks", async () => { + fileMock.enqueueResult([new Uint8Array([1]), new Uint8Array([2])]); + + const stream = await file.openDownloadStream("ns", "Doc", "blob", "rec-1"); + + const chunks: unknown[] = []; + for await (const chunk of stream) { + chunks.push(chunk); + } + + expect(chunks).toEqual([new Uint8Array([1]), new Uint8Array([2])]); + expect(fileMock.calls[0]?.method).toBe("openDownloadStream"); + }); + + test("TailorDBFileError type alias resolves to globalThis class", () => { + const TailorDBFileError = ( + globalThis as unknown as { + TailorDBFileError: new (m: string, c?: string) => Error & { code?: string }; + } + ).TailorDBFileError; + const err = new TailorDBFileError("not found", "NOT_FOUND"); + expect(err.name).toBe("TailorDBFileError"); + expect(err.code).toBe("NOT_FOUND"); + // Type-level: file.TailorDBFileError is the global class + const _typed: file.TailorDBFileError = err as file.TailorDBFileError; + expect(_typed).toBe(err); + }); +}); diff --git a/packages/sdk/src/runtime/__tests__/globals.test.ts b/packages/sdk/src/runtime/__tests__/globals.test.ts new file mode 100644 index 000000000..fc9496fbb --- /dev/null +++ b/packages/sdk/src/runtime/__tests__/globals.test.ts @@ -0,0 +1,41 @@ +/** + * Type-level tests confirming that importing the runtime entries activates + * the ambient `tailor.*` / `tailordb` globals declared in + * `src/runtime/globals.ts`. + * + * These assertions are type-only — they reference `tailor`, `tailordb`, and + * `TailorDBFileError` solely through `typeof` so the test does not require + * the platform runtime to inject those values into the unit test environment. + */ +import "@/runtime"; +import { describe, expectTypeOf, test } from "vitest"; + +describe("@tailor-platform/sdk/runtime activates ambient globals", () => { + test("tailor.iconv.convert is declared as a function", () => { + expectTypeOf().toBeFunction(); + }); + + test("tailor.secretmanager.getSecret returns Promise", () => { + expectTypeOf>().toEqualTypeOf< + Promise + >(); + }); + + test("tailor.workflow.triggerWorkflow returns Promise", () => { + expectTypeOf>().toEqualTypeOf< + Promise + >(); + }); + + test("tailor.context.Invoker is exposed as a namespace type", () => { + expectTypeOf().not.toBeAny(); + }); + + test("tailordb.file.upload is declared as a function", () => { + expectTypeOf().toBeFunction(); + }); + + test("TailorDBFileError is declared as a global class", () => { + expectTypeOf().not.toBeAny(); + }); +}); diff --git a/packages/sdk/src/runtime/__tests__/iconv.test.ts b/packages/sdk/src/runtime/__tests__/iconv.test.ts new file mode 100644 index 000000000..8b08d19c6 --- /dev/null +++ b/packages/sdk/src/runtime/__tests__/iconv.test.ts @@ -0,0 +1,105 @@ +/** + * Tests for `@tailor-platform/sdk/runtime/iconv` typed wrappers. + * + * Verifies that each wrapper forwards to `globalThis.tailor.iconv.*` (recorded + * via `iconvMock.calls`) and that the return-type narrowing (`UTF-8` → + * `string`, otherwise `Uint8Array`) holds at the type level. + */ +import "@/runtime/globals"; +import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; +import * as iconv from "@/runtime/iconv"; +import { cleanupMocks, iconvMock, injectMocks } from "@/vitest/mock"; + +describe("@tailor-platform/sdk/runtime/iconv", () => { + beforeEach(() => { + injectMocks(globalThis); + iconvMock.reset(); + }); + + afterEach(() => { + cleanupMocks(globalThis); + }); + + test("convert forwards args and returns string for UTF-8 target", () => { + iconvMock.setResolver((method, args) => { + if (method === "convert" && args[2] === "UTF-8") return "decoded"; + return undefined; + }); + + const out = iconv.convert(new Uint8Array([0x61]), "Shift_JIS", "UTF-8"); + + expect(out).toBe("decoded"); + expectTypeOf(out).toEqualTypeOf(); + expect(iconvMock.calls).toEqual([ + { method: "convert", args: [new Uint8Array([0x61]), "Shift_JIS", "UTF-8"] }, + ]); + }); + + test("convert returns Uint8Array for non-UTF-8 target", () => { + iconvMock.setResolver(() => new Uint8Array([0x82, 0xa0])); + + const out = iconv.convert("あ", "UTF-8", "Shift_JIS"); + + expect(out).toBeInstanceOf(Uint8Array); + expectTypeOf(out).toEqualTypeOf(); + }); + + test("convertBuffer forwards and narrows return type", () => { + iconvMock.setResolver(() => "ok"); + + const out = iconv.convertBuffer(new Uint8Array(), "Shift_JIS", "UTF-8"); + + expect(out).toBe("ok"); + expectTypeOf(out).toEqualTypeOf(); + expect(iconvMock.calls[0]).toMatchObject({ method: "convertBuffer" }); + }); + + test("decode forwards args and returns string", () => { + iconvMock.setResolver(() => "hello"); + + const out = iconv.decode(new Uint8Array([0x68]), "ASCII"); + + expect(out).toBe("hello"); + expectTypeOf(out).toEqualTypeOf(); + expect(iconvMock.calls[0]).toMatchObject({ + method: "decode", + args: [new Uint8Array([0x68]), "ASCII"], + }); + }); + + test("encode narrows return type by encoding", () => { + iconvMock.setResolver((_method, args) => (args[1] === "UTF-8" ? "x" : new Uint8Array([1]))); + + const utf8 = iconv.encode("a", "UTF-8"); + const sjis = iconv.encode("a", "Shift_JIS"); + + expectTypeOf(utf8).toEqualTypeOf(); + expectTypeOf(sjis).toEqualTypeOf(); + expect(utf8).toBe("x"); + expect(sjis).toBeInstanceOf(Uint8Array); + }); + + test("encodings forwards and returns string[]", () => { + iconvMock.setResolver(() => ["UTF-8", "Shift_JIS"]); + + const list = iconv.encodings(); + + expect(list).toEqual(["UTF-8", "Shift_JIS"]); + expectTypeOf(list).toEqualTypeOf(); + }); + + test("Iconv class delegates convert to global Iconv", () => { + iconvMock.setResolver((method) => (method === "convert" ? "via-class" : undefined)); + + const conv = new iconv.Iconv("Shift_JIS", "UTF-8"); + const out = conv.convert(new Uint8Array([0x61])); + + expect(out).toBe("via-class"); + expect(iconvMock.calls).toEqual([ + { + method: "convert", + args: [new Uint8Array([0x61]), "Shift_JIS", "UTF-8"], + }, + ]); + }); +}); diff --git a/packages/sdk/src/runtime/__tests__/idp.test.ts b/packages/sdk/src/runtime/__tests__/idp.test.ts new file mode 100644 index 000000000..2d8e47f20 --- /dev/null +++ b/packages/sdk/src/runtime/__tests__/idp.test.ts @@ -0,0 +1,87 @@ +/** + * Tests for `@tailor-platform/sdk/runtime/idp` typed wrappers. + * + * Verifies that {@link idp.Client} forwards each method to the platform's + * `tailor.idp.Client` and records calls with method, args, and namespace. + */ +import "@/runtime/globals"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import * as idp from "@/runtime/idp"; +import { cleanupMocks, idpMock, injectMocks } from "@/vitest/mock"; + +describe("@tailor-platform/sdk/runtime/idp", () => { + beforeEach(() => { + injectMocks(globalThis); + idpMock.reset(); + }); + + afterEach(() => { + cleanupMocks(globalThis); + }); + + test("Client.user forwards args and namespace", async () => { + idpMock.enqueueResult({ id: "u-1", name: "alice", disabled: false }); + + const client = new idp.Client({ namespace: "ns" }); + const result = await client.user("u-1"); + + expect(result).toEqual({ id: "u-1", name: "alice", disabled: false }); + expect(idpMock.calls).toEqual([{ method: "user", args: ["u-1"], namespace: "ns" }]); + }); + + test("Client.userByName forwards", async () => { + idpMock.enqueueResult({ id: "u-1", name: "alice", disabled: false }); + + const client = new idp.Client({ namespace: "ns" }); + await client.userByName("alice"); + + expect(idpMock.calls).toEqual([{ method: "userByName", args: ["alice"], namespace: "ns" }]); + }); + + test("Client.users forwards options", async () => { + idpMock.enqueueResult({ + users: [{ id: "u-1", name: "alice", disabled: false }], + nextPageToken: null, + totalCount: 1, + }); + + const client = new idp.Client({ namespace: "ns" }); + const result = await client.users({ first: 10 }); + + expect(result.totalCount).toBe(1); + expect(idpMock.calls).toEqual([{ method: "users", args: [{ first: 10 }], namespace: "ns" }]); + }); + + test("Client.createUser / updateUser / deleteUser forward", async () => { + idpMock.enqueueResults( + { id: "u-2", name: "bob", disabled: false }, + { id: "u-2", name: "bob2", disabled: false }, + true, + ); + + const client = new idp.Client({ namespace: "ns" }); + await client.createUser({ name: "bob", password: "p" }); + await client.updateUser({ id: "u-2", name: "bob2" }); + const removed = await client.deleteUser("u-2"); + + expect(removed).toBe(true); + expect(idpMock.calls.map((c) => c.method)).toEqual(["createUser", "updateUser", "deleteUser"]); + }); + + test("Client.sendPasswordResetEmail forwards", async () => { + const client = new idp.Client({ namespace: "ns" }); + const ok = await client.sendPasswordResetEmail({ + userId: "u-1", + redirectUri: "https://example.com/reset", + }); + + expect(ok).toBe(true); + expect(idpMock.calls).toEqual([ + { + method: "sendPasswordResetEmail", + args: [{ userId: "u-1", redirectUri: "https://example.com/reset" }], + namespace: "ns", + }, + ]); + }); +}); diff --git a/packages/sdk/src/runtime/__tests__/secretmanager.test.ts b/packages/sdk/src/runtime/__tests__/secretmanager.test.ts new file mode 100644 index 000000000..69a61b6e6 --- /dev/null +++ b/packages/sdk/src/runtime/__tests__/secretmanager.test.ts @@ -0,0 +1,52 @@ +/** + * Tests for `@tailor-platform/sdk/runtime/secretmanager` typed wrappers. + */ +import "@/runtime/globals"; +import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; +import * as secretmanager from "@/runtime/secretmanager"; +import { cleanupMocks, injectMocks, secretmanagerMock } from "@/vitest/mock"; + +describe("@tailor-platform/sdk/runtime/secretmanager", () => { + beforeEach(() => { + injectMocks(globalThis); + secretmanagerMock.reset(); + }); + + afterEach(() => { + cleanupMocks(globalThis); + }); + + test("getSecret forwards to global and returns Promise", async () => { + secretmanagerMock.setSecrets({ vault: { API_KEY: "sk-123" } }); + + const result = secretmanager.getSecret("vault", "API_KEY"); + + expectTypeOf(result).toEqualTypeOf>(); + await expect(result).resolves.toBe("sk-123"); + expect(secretmanagerMock.calls).toEqual([ + { method: "getSecret", vault: "vault", name: "API_KEY" }, + ]); + }); + + test("getSecret returns undefined for missing secret", async () => { + await expect(secretmanager.getSecret("vault", "NOPE")).resolves.toBeUndefined(); + }); + + test("getSecrets narrows record key to const tuple union", async () => { + secretmanagerMock.setSecrets({ v: { a: "1", b: "2" } }); + + const result = secretmanager.getSecrets("v", ["a", "b"] as const); + + expectTypeOf(result).toEqualTypeOf>>>(); + await expect(result).resolves.toEqual({ a: "1", b: "2" }); + expect(secretmanagerMock.calls).toEqual([ + { method: "getSecrets", vault: "v", names: ["a", "b"] }, + ]); + }); + + test("getSecrets omits missing names", async () => { + secretmanagerMock.setSecrets({ v: { a: "1" } }); + + await expect(secretmanager.getSecrets("v", ["a", "b"] as const)).resolves.toEqual({ a: "1" }); + }); +}); diff --git a/packages/sdk/src/runtime/__tests__/workflow.test.ts b/packages/sdk/src/runtime/__tests__/workflow.test.ts new file mode 100644 index 000000000..1ac792c4b --- /dev/null +++ b/packages/sdk/src/runtime/__tests__/workflow.test.ts @@ -0,0 +1,73 @@ +/** + * Tests for `@tailor-platform/sdk/runtime/workflow` typed wrappers. + */ +import "@/runtime/globals"; +import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; +import * as workflow from "@/runtime/workflow"; +import { cleanupMocks, injectMocks, workflowMock } from "@/vitest/mock"; + +describe("@tailor-platform/sdk/runtime/workflow", () => { + beforeEach(() => { + injectMocks(globalThis); + workflowMock.reset(); + }); + + afterEach(() => { + cleanupMocks(globalThis); + }); + + test("triggerWorkflow forwards args and returns Promise", async () => { + workflowMock.setWorkflowExecutionId("exec-42"); + + const promise = workflow.triggerWorkflow("my-workflow", { a: 1 }); + + expectTypeOf(promise).toEqualTypeOf>(); + await expect(promise).resolves.toBe("exec-42"); + expect(workflowMock.calls).toEqual([ + { method: "triggerWorkflow", args: ["my-workflow", { a: 1 }, undefined] }, + ]); + }); + + test("triggerWorkflow forwards options", async () => { + await workflow.triggerWorkflow( + "my-workflow", + { a: 1 }, + { + authInvoker: { namespace: "ns", machineUserName: "mu" }, + }, + ); + + expect(workflowMock.calls[0]?.args[2]).toEqual({ + authInvoker: { namespace: "ns", machineUserName: "mu" }, + }); + }); + + test("triggerJobFunction forwards and returns enqueued result", () => { + workflowMock.enqueueResult({ ok: true }); + + const result = workflow.triggerJobFunction("my-job", { id: 1 }); + + expect(result).toEqual({ ok: true }); + expect(workflowMock.triggeredJobs).toEqual([{ jobName: "my-job", args: { id: 1 } }]); + }); + + test("wait records the call and returns the configured result", () => { + workflowMock.setWaitResult({ resumed: true }); + + const result = workflow.wait("key-1", { p: 1 }); + + expect(result).toEqual({ resumed: true }); + expect(workflowMock.calls).toEqual([{ method: "wait", args: ["key-1", { p: 1 }] }]); + }); + + test("resolve records the call without invoking the callback", async () => { + let invoked = false; + await workflow.resolve("exec-1", "key-1", () => { + invoked = true; + }); + + expect(invoked).toBe(false); + expect(workflowMock.calls).toHaveLength(1); + expect(workflowMock.calls[0]?.method).toBe("resolve"); + }); +}); diff --git a/packages/sdk/src/runtime/authconnection.ts b/packages/sdk/src/runtime/authconnection.ts new file mode 100644 index 000000000..3736644cf --- /dev/null +++ b/packages/sdk/src/runtime/authconnection.ts @@ -0,0 +1,23 @@ +/** + * Auth connection utilities. + * + * Thin typed wrapper around the platform-provided `tailor.authconnection` runtime API. + * At runtime this delegates to `globalThis.tailor.authconnection`. Use + * `authconnectionMock` from `@tailor-platform/sdk/vitest` to mock in unit tests. + * @example + * import { authconnection } from "@tailor-platform/sdk/runtime"; + * + * const token = await authconnection.getConnectionToken("my-connection"); + */ + +import "./globals"; + +/** + * Returns the access token for the given auth connection. + * @param connectionName - Auth connection name as defined in tailor.config + * @returns Token payload (provider-specific shape) + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getConnectionToken(connectionName: string): Promise { + return tailor.authconnection.getConnectionToken(connectionName); +} diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts new file mode 100644 index 000000000..1456a3825 --- /dev/null +++ b/packages/sdk/src/runtime/context.ts @@ -0,0 +1,28 @@ +/** + * Execution context utilities. + * + * Thin typed wrapper around the platform-provided `tailor.context` runtime API. + * At runtime this delegates to `globalThis.tailor.context`. Use + * `setupInvokerMock` from `@tailor-platform/sdk/test` to mock in unit tests. + * @example + * import { context } from "@tailor-platform/sdk/runtime"; + * + * const invoker = context.getInvoker(); + * if (invoker) { + * console.log(invoker.id, invoker.type); + * } + */ + +import "./globals"; + +/** Re-exported invoker type from the global runtime. */ +export type Invoker = tailor.context.Invoker; + +/** + * Returns information about the invoker of the current function execution, + * or `null` for anonymous invocations. + * @returns Invoker details, or `null` when the call is anonymous + */ +export function getInvoker(): Invoker | null { + return tailor.context.getInvoker(); +} diff --git a/packages/sdk/src/runtime/file.ts b/packages/sdk/src/runtime/file.ts new file mode 100644 index 000000000..6a6223daf --- /dev/null +++ b/packages/sdk/src/runtime/file.ts @@ -0,0 +1,145 @@ +/** + * TailorDB file (BLOB) utilities. + * + * Thin typed wrapper around the platform-provided `tailordb.file` runtime API. + * At runtime this delegates to `globalThis.tailordb.file`. Use `fileMock` from + * `@tailor-platform/sdk/vitest` to mock these calls in unit tests. + * @example + * import { file } from "@tailor-platform/sdk/runtime"; + * + * const { metadata } = await file.upload( + * "my-namespace", + * "Document", + * "attachment", + * recordId, + * bytes, + * ); + */ + +import "./globals"; + +export type UploadMetadata = globalThis.UploadMetadata; +export type DownloadMetadata = globalThis.DownloadMetadata; +export type FileMetadata = globalThis.FileMetadata; +export type StreamMetadata = globalThis.StreamMetadata; +export type FileUploadOptions = globalThis.FileUploadOptions; +export type FileUploadResponse = globalThis.FileUploadResponse; +export type FileDownloadResponse = globalThis.FileDownloadResponse; +export type FileDownloadAsBase64Response = globalThis.FileDownloadAsBase64Response; +export type StreamValue = globalThis.StreamValue; +export type FileStreamIterator = globalThis.FileStreamIterator; +export type TailorDBFileError = globalThis.TailorDBFileError; + +/** + * Upload a file to TailorDB. + * @param namespace - TailorDB namespace + * @param typeName - TailorDB type name + * @param fieldName - File field name on the type + * @param recordId - Record ID owning the field + * @param data - File contents + * @param options - Upload options (e.g. `contentType`) + * @returns Upload response containing the file metadata + */ +export function upload( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + data: string | ArrayBuffer | Uint8Array | number[], + options?: FileUploadOptions, +): Promise { + return tailordb.file.upload(namespace, typeName, fieldName, recordId, data, options); +} + +/** + * Download a file from TailorDB. + * + * Throws `TailorDBFileError` with code `FILE_TOO_LARGE` when the file + * exceeds 10MB — use {@link openDownloadStream} for large files. + * @param namespace - TailorDB namespace + * @param typeName - TailorDB type name + * @param fieldName - File field name on the type + * @param recordId - Record ID owning the field + * @returns Bytes and metadata for the file + */ +export function download( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, +): Promise { + return tailordb.file.download(namespace, typeName, fieldName, recordId); +} + +/** + * Download a file from TailorDB as a Base64-encoded string. + * + * Throws `TailorDBFileError` with code `FILE_TOO_LARGE` when the file + * exceeds 10MB — use {@link openDownloadStream} for large files. + * @param namespace - TailorDB namespace + * @param typeName - TailorDB type name + * @param fieldName - File field name on the type + * @param recordId - Record ID owning the field + * @returns Base64-encoded contents and metadata for the file + */ +export function downloadAsBase64( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, +): Promise { + return tailordb.file.downloadAsBase64(namespace, typeName, fieldName, recordId); +} + +/** + * Delete a file from TailorDB. + * @param namespace - TailorDB namespace + * @param typeName - TailorDB type name + * @param fieldName - File field name on the type + * @param recordId - Record ID owning the field + * @returns Resolves once the file has been deleted + */ +function deleteFile( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, +): Promise { + return tailordb.file.delete(namespace, typeName, fieldName, recordId); +} + +/** + * Get file metadata from TailorDB. + * @param namespace - TailorDB namespace + * @param typeName - TailorDB type name + * @param fieldName - File field name on the type + * @param recordId - Record ID owning the field + * @returns Metadata for the stored file + */ +export function getMetadata( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, +): Promise { + return tailordb.file.getMetadata(namespace, typeName, fieldName, recordId); +} + +/** + * Open a download stream for large files. + * @param namespace - TailorDB namespace + * @param typeName - TailorDB type name + * @param fieldName - File field name on the type + * @param recordId - Record ID owning the field + * @returns Async iterator yielding file chunks; call `close()` to release resources + */ +export function openDownloadStream( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, +): Promise { + return tailordb.file.openDownloadStream(namespace, typeName, fieldName, recordId); +} + +export { deleteFile as delete }; diff --git a/packages/sdk/src/runtime/globals.ts b/packages/sdk/src/runtime/globals.ts new file mode 100644 index 000000000..12a395f63 --- /dev/null +++ b/packages/sdk/src/runtime/globals.ts @@ -0,0 +1,545 @@ +/** + * Ambient global type definitions for the Tailor Platform Function runtime. + * + * The Tailor Platform Function runtime injects `tailor.*` and `tailordb` + * objects into the global scope. This file declares their type signatures so + * they can be referenced from any TypeScript code that runs in (or is bundled + * for) the runtime. + * @example + * // Side-effect import to enable the global types in a single file: + * import "@tailor-platform/sdk/runtime/globals"; + * + * // Or register globally in tsconfig.json: + * // "compilerOptions": { "types": ["@tailor-platform/sdk/runtime/globals"] } + * + * Most users do not need to import this directly — `@tailor-platform/sdk/runtime` + * exposes typed wrappers that cover the same surface without relying on globals. + */ + +/* eslint-disable @typescript-eslint/no-namespace, jsdoc/require-param, jsdoc/require-returns, jsdoc/require-param-description */ +declare global { + namespace Tailordb { + class Client { + constructor(config: { namespace: string }); + connect(): Promise; + end(): Promise; + queryObject(sql: string, args?: readonly unknown[]): Promise>; + } + + interface QueryResult { + rows: T[]; + command: CommandType; + rowCount: number; + } + + type CommandType = + | "INSERT" + | "DELETE" + | "UPDATE" + | "SELECT" + | "MOVE" + | "FETCH" + | "COPY" + | "CREATE"; + } + + // eslint-disable-next-line no-var + var tailordb: { + Client: typeof Tailordb.Client; + file: TailorDBFileAPI; + }; + + namespace tailor.secretmanager { + /** + * getSecrets returns multiple secret objects (key = name, value = secret) + * at once according to vault and secret names. + * + * If a secret does not exist, it will not be included in the result. + * @param vault + * @param names + */ + function getSecrets( + vault: string, + names: T, + ): Promise>>; + + /** + * getSecret returns a secret according to vault and name. + * + * If the secret does not exist, undefined is returned. + * @param vault + * @param name + */ + function getSecret(vault: string, name: string): Promise; + } + + namespace tailor.authconnection { + /** + * getConnectionToken returns the access token for an auth connection + * @param connectionName + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function getConnectionToken(connectionName: string): Promise; + } + + namespace tailor.iconv { + /** + * Convert string from one encoding to another + * @param str + * @param fromEncoding + * @param toEncoding + */ + function convert( + str: string | Uint8Array | ArrayBuffer, + fromEncoding: string, + toEncoding: T, + ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; + + /** + * Convert buffer from one encoding to another + * @param buffer + * @param fromEncoding + * @param toEncoding + */ + function convertBuffer( + buffer: Uint8Array | ArrayBuffer, + fromEncoding: string, + toEncoding: T, + ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; + + /** + * Decode buffer to string + * @param buffer + * @param encoding + */ + function decode(buffer: Uint8Array | ArrayBuffer, encoding: string): string; + + /** + * Encode string to buffer + * @param str + * @param encoding + */ + function encode( + str: string, + encoding: T, + ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; + + /** + * Get list of supported encodings + */ + function encodings(): string[]; + + /** + * Iconv class for compatibility with node-iconv + */ + class Iconv { + constructor(fromEncoding: string, toEncoding: string); + convert(input: string | Uint8Array | ArrayBuffer): string | Uint8Array; + } + } + + // TailorDB File Extension Types + + /** + * Custom error class for TailorDB File operations + */ + class TailorDBFileError extends Error { + name: "TailorDBFileError"; + code?: + | "INVALID_PARAMS" + | "INVALID_DATA_TYPE" + | "OPERATION_FAILED" + | "DELETE_FAILED" + | "STREAM_OPEN_FAILED" + | "STREAM_READ_ERROR" + | "STREAM_ERROR" + | "FILE_TOO_LARGE"; + cause?: unknown; + } + + /** + * Upload response metadata + */ + interface UploadMetadata { + fileSize: number; + sha256sum: string; + } + + /** + * Download response metadata + */ + interface DownloadMetadata { + contentType: string; + fileSize: number; + sha256sum: string; + lastUploadedAt: string; + } + + /** + * File metadata (for getMetadata API) + */ + interface FileMetadata { + contentType: string; + fileSize: number; + sha256sum: string; + urlPath: string; + lastUploadedAt?: string; + } + + /** + * Stream metadata (first chunk) + */ + interface StreamMetadata { + contentType: string; + fileSize: number; + sha256sum: string; + } + + /** + * Upload options interface + */ + interface FileUploadOptions { + contentType?: string; + } + + /** + * Upload response interface + */ + interface FileUploadResponse { + metadata: UploadMetadata; + } + + /** + * Download response interface + */ + interface FileDownloadResponse { + data: Uint8Array; + metadata: DownloadMetadata; + } + + /** + * Download as Base64 response interface + */ + interface FileDownloadAsBase64Response { + data: string; + metadata: DownloadMetadata; + } + + /** + * Stream chunk types + */ + type StreamValue = + | { type: "metadata"; metadata: StreamMetadata } + | { type: "chunk"; data: Uint8Array; position: number } + | { type: "complete" }; + + /** + * Stream iterator interface + */ + interface FileStreamIterator extends AsyncIterableIterator { + next(): Promise>; + close(): Promise; + } + + /** + * TailorDB File API + */ + interface TailorDBFileAPI { + /** + * Upload a file to TailorDB + */ + upload( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + data: string | ArrayBuffer | Uint8Array | number[], + options?: FileUploadOptions, + ): Promise; + + /** + * Download a file from TailorDB + * @throws {TailorDBFileError} FILE_TOO_LARGE if file exceeds 10MB - use openDownloadStream() for large files + */ + download( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + ): Promise; + + /** + * Download a file from TailorDB as Base64 string. + * Unlike download which returns decoded binary data (Uint8Array), + * this returns the raw Base64-encoded string for use cases requiring + * Base64 format (e.g., embedding in JSON responses, data URIs). + * @throws {TailorDBFileError} FILE_TOO_LARGE if file exceeds 10MB - use openDownloadStream() for large files + */ + downloadAsBase64( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + ): Promise; + + /** + * Delete a file from TailorDB + */ + delete(namespace: string, typeName: string, fieldName: string, recordId: string): Promise; + + /** + * Get file metadata from TailorDB + */ + getMetadata( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + ): Promise; + + /** + * Open a download stream for large files + */ + openDownloadStream( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + ): Promise; + } + + namespace tailor.idp { + /** + * Configuration for creating an IDP Client + */ + interface ClientConfig { + namespace: string; + } + + /** + * User object returned from IDP operations + */ + interface User { + id: string; + name: string; + disabled: boolean; + createdAt?: string; + updatedAt?: string; + } + + /** + * Query options for filtering users + */ + interface UserQuery { + /** Filter by user IDs */ + ids?: string[]; + /** Filter by user names */ + names?: string[]; + } + + /** + * Options for listing users + */ + interface ListUsersOptions { + /** Maximum number of users to return */ + first?: number; + /** Page token for pagination */ + after?: string; + /** Query filter for users */ + query?: UserQuery; + } + + /** + * Response from listing users + */ + interface ListUsersResponse { + users: User[]; + nextPageToken: string | null; + totalCount: number; + } + + /** + * Input for creating a new user + */ + interface CreateUserInput { + /** The user's name (typically email) */ + name: string; + /** The user's password. If omitted, the user is created without a password (cannot log in with any password). */ + password?: string; + /** Whether the user is disabled */ + disabled?: boolean; + } + + /** + * Input for updating an existing user + */ + interface UpdateUserInput { + /** The user's ID */ + id: string; + /** New name for the user */ + name?: string; + /** New password for the user. Cannot be used with clearPassword. */ + password?: string; + /** If true, remove the user's password. Cannot be used with password. */ + clearPassword?: boolean; + /** New disabled status for the user */ + disabled?: boolean; + } + + /** + * Input for sending a password reset email + */ + interface SendPasswordResetEmailInput { + /** The ID of the user */ + userId: string; + /** The URI to redirect to after password reset */ + redirectUri: string; + /** The sender display name. Defaults to 'Tailor Platform IdP'. */ + fromName?: string; + /** The email subject line. Defaults to the localized default subject. */ + subject?: string; + } + + /** + * IDP Client for user management operations + */ + class Client { + constructor(config: ClientConfig); + + /** + * List users in the namespace with optional filtering and pagination. + */ + users(options?: ListUsersOptions): Promise; + + /** + * Get a user by ID. + */ + user(userId: string): Promise; + + /** + * Get a user by name. + */ + userByName(name: string): Promise; + + /** + * Create a new user. + */ + createUser(input: CreateUserInput): Promise; + + /** + * Update an existing user. + */ + updateUser(input: UpdateUserInput): Promise; + + /** + * Delete a user by ID. + * @returns True if successful + */ + deleteUser(userId: string): Promise; + + /** + * Send a password reset email to a user. + * @returns True if successful + */ + sendPasswordResetEmail(input: SendPasswordResetEmailInput): Promise; + } + } + + namespace tailor.workflow { + /** + * Specifies the machine user that should be used to execute the workflow. + * This allows workflows to run with specific authentication context. + */ + interface AuthInvoker { + /** The namespace where the machine user is defined */ + namespace: string; + /** The name of the machine user to use for workflow execution */ + machineUserName: string; + } + + /** + * Options for triggering a workflow + */ + interface TriggerWorkflowOptions { + /** Optional authentication invoker to specify which machine user should execute the workflow */ + authInvoker?: AuthInvoker; + } + + /** + * Triggers a workflow and returns its execution ID. + * @param workflow_name + * @param args + * @param options + */ + function triggerWorkflow( + workflow_name: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args?: any, + options?: TriggerWorkflowOptions, + ): Promise; + + /** + * Triggers a job function and returns its result. + * @param job_name + * @param args + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function triggerJobFunction(job_name: string, args?: any): any; + + /** + * Suspends the current workflow execution and waits for an external signal to resume. + * The workflow will be parked in "Waiting" status until resolved via `resolve()`. + * @param key + * @param payload + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function wait(key: string, payload?: any): any; + + /** + * Resolves a waiting workflow execution, causing it to resume. + * The callback receives the wait payload and must return a JSON-serializable result + * that will be passed back to the `wait()` caller. + * @param executionId + * @param key + * @param callback + */ + function resolve( + executionId: string, + key: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback: (waitPayload: any) => any, + ): Promise; + } + + namespace tailor.context { + /** + * Information about the invoker of the current function execution. + */ + interface Invoker { + /** The invoker's ID */ + id: string; + /** The invoker's type */ + type: "user" | "machine_user"; + /** The workspace ID */ + workspaceId: string; + /** The invoker's attribute IDs */ + attributes: string[]; + /** The invoker's attribute map */ + attributeMap: Record; + } + + /** + * Returns information about the invoker of the current function execution, + * or `null` for anonymous invocations. + */ + function getInvoker(): Invoker | null; + } +} + +/** + * Sentinel marker so that bundlers retain this module's `declare global` block + * in the emitted `.d.mts` instead of tree-shaking it down to `export {}`. + * Not part of the public SDK API. + * @internal + */ +export const __TAILOR_RUNTIME_GLOBALS_LOADED__: true = true; diff --git a/packages/sdk/src/runtime/iconv.ts b/packages/sdk/src/runtime/iconv.ts new file mode 100644 index 000000000..a7c95d408 --- /dev/null +++ b/packages/sdk/src/runtime/iconv.ts @@ -0,0 +1,104 @@ +/** + * Character encoding conversion utilities. + * + * Thin typed wrapper around the platform-provided `tailor.iconv` runtime API. + * At runtime this delegates to `globalThis.tailor.iconv`, which is provided by + * the Tailor Platform Function runtime. Use `iconvMock` from + * `@tailor-platform/sdk/vitest` to mock these calls in unit tests. + * @example + * import { iconv } from "@tailor-platform/sdk/runtime"; + * + * const utf8 = iconv.convert(sjisBuffer, "Shift_JIS", "UTF-8"); // string + * const sjis = iconv.convert("こんにちは", "UTF-8", "Shift_JIS"); // Uint8Array + * + * const conv = new iconv.Iconv("Shift_JIS", "UTF-8"); + * const out = conv.convert(sjisBuffer); + */ + +import "./globals"; + +/** + * Convert a string or buffer between encodings. + * @param str - Input data to convert + * @param fromEncoding - Source encoding name + * @param toEncoding - Target encoding name + * @returns `string` when `toEncoding` is `"UTF8"` or `"UTF-8"`, otherwise `Uint8Array`. + */ +export function convert( + str: string | Uint8Array | ArrayBuffer, + fromEncoding: string, + toEncoding: T, +): T extends "UTF8" | "UTF-8" ? string : Uint8Array { + return tailor.iconv.convert(str, fromEncoding, toEncoding); +} + +/** + * Convert a buffer between encodings. + * @param buffer - Input bytes to convert + * @param fromEncoding - Source encoding name + * @param toEncoding - Target encoding name + * @returns `string` when `toEncoding` is `"UTF8"` or `"UTF-8"`, otherwise `Uint8Array`. + */ +export function convertBuffer( + buffer: Uint8Array | ArrayBuffer, + fromEncoding: string, + toEncoding: T, +): T extends "UTF8" | "UTF-8" ? string : Uint8Array { + return tailor.iconv.convertBuffer(buffer, fromEncoding, toEncoding); +} + +/** + * Decode a buffer to a UTF-8 string using the given source encoding. + * @param buffer - Input bytes + * @param encoding - Source encoding name + * @returns Decoded UTF-8 string + */ +export function decode(buffer: Uint8Array | ArrayBuffer, encoding: string): string { + return tailor.iconv.decode(buffer, encoding); +} + +/** + * Encode a UTF-8 string into the given target encoding. + * @param str - Input string + * @param encoding - Target encoding name + * @returns `string` when `encoding` is `"UTF8"` or `"UTF-8"`, otherwise `Uint8Array`. + */ +export function encode( + str: string, + encoding: T, +): T extends "UTF8" | "UTF-8" ? string : Uint8Array { + return tailor.iconv.encode(str, encoding); +} + +/** + * Returns the list of supported encoding names. + * @returns Array of encoding names supported by the platform iconv runtime + */ +export function encodings(): string[] { + return tailor.iconv.encodings(); +} + +interface IconvImpl { + convert(input: string | Uint8Array | ArrayBuffer): string | Uint8Array; +} + +/** + * Stateful converter for repeated conversions between a fixed encoding pair. + * Compatible with the `node-iconv` API surface. + */ +export class Iconv { + private impl: IconvImpl; + + constructor(fromEncoding: string, toEncoding: string) { + this.impl = new tailor.iconv.Iconv(fromEncoding, toEncoding); + } + + /** + * Convert input using this converter's fixed encoding pair. + * @param input - Bytes or string to convert + * @returns Encoded output (string for UTF-8 targets, otherwise `Uint8Array`). + */ + convert(input: string | Uint8Array | ArrayBuffer): string | Uint8Array { + return this.impl.convert(input); + } +} diff --git a/packages/sdk/src/runtime/idp.ts b/packages/sdk/src/runtime/idp.ts new file mode 100644 index 000000000..545504792 --- /dev/null +++ b/packages/sdk/src/runtime/idp.ts @@ -0,0 +1,114 @@ +/** + * IDP (Identity Provider) utilities. + * + * Thin typed wrapper around the platform-provided `tailor.idp` runtime API. + * At runtime this delegates to `globalThis.tailor.idp`. Use `idpMock` from + * `@tailor-platform/sdk/vitest` to mock these calls in unit tests. + * @example + * import { idp } from "@tailor-platform/sdk/runtime"; + * + * const client = new idp.Client({ namespace: "my-namespace" }); + * const { users } = await client.users({ first: 10 }); + */ + +import "./globals"; + +/** Configuration object for {@link Client}. */ +export type ClientConfig = tailor.idp.ClientConfig; + +/** User record returned by IDP operations. */ +export type User = tailor.idp.User; + +/** Filter options for {@link Client.users}. */ +export type UserQuery = tailor.idp.UserQuery; + +/** Pagination/filter options for {@link Client.users}. */ +export type ListUsersOptions = tailor.idp.ListUsersOptions; + +/** Response shape for {@link Client.users}. */ +export type ListUsersResponse = tailor.idp.ListUsersResponse; + +/** Input for {@link Client.createUser}. */ +export type CreateUserInput = tailor.idp.CreateUserInput; + +/** Input for {@link Client.updateUser}. */ +export type UpdateUserInput = tailor.idp.UpdateUserInput; + +/** Input for {@link Client.sendPasswordResetEmail}. */ +export type SendPasswordResetEmailInput = tailor.idp.SendPasswordResetEmailInput; + +/** + * IDP Client for user management operations. + * + * Wraps the platform-provided `tailor.idp.Client` and exposes the same surface. + */ +export class Client { + private impl: tailor.idp.Client; + + constructor(config: ClientConfig) { + this.impl = new tailor.idp.Client(config); + } + + /** + * List users in the namespace with optional filtering and pagination. + * @param options - Pagination and filter options + * @returns Page of users with `nextPageToken` and `totalCount` + */ + users(options?: ListUsersOptions): Promise { + return this.impl.users(options); + } + + /** + * Get a user by ID. + * @param userId - IDP user ID + * @returns The matching user + */ + user(userId: string): Promise { + return this.impl.user(userId); + } + + /** + * Get a user by name. + * @param name - IDP user name + * @returns The matching user + */ + userByName(name: string): Promise { + return this.impl.userByName(name); + } + + /** + * Create a new user. + * @param input - User attributes + * @returns The newly created user + */ + createUser(input: CreateUserInput): Promise { + return this.impl.createUser(input); + } + + /** + * Update an existing user. + * @param input - User ID plus attributes to update + * @returns The updated user + */ + updateUser(input: UpdateUserInput): Promise { + return this.impl.updateUser(input); + } + + /** + * Delete a user by ID. + * @param userId - IDP user ID + * @returns `true` when the user was deleted + */ + deleteUser(userId: string): Promise { + return this.impl.deleteUser(userId); + } + + /** + * Send a password reset email to a user. + * @param input - Target user ID and redirect URI + * @returns `true` when the email was queued + */ + sendPasswordResetEmail(input: SendPasswordResetEmailInput): Promise { + return this.impl.sendPasswordResetEmail(input); + } +} diff --git a/packages/sdk/src/runtime/index.ts b/packages/sdk/src/runtime/index.ts new file mode 100644 index 000000000..3f56bc307 --- /dev/null +++ b/packages/sdk/src/runtime/index.ts @@ -0,0 +1,31 @@ +/** + * Typed wrappers for the Tailor Platform Function runtime APIs. + * + * Each namespace mirrors the corresponding `tailor.*` (or `tailordb.file`) + * surface that the platform runtime exposes globally, so consumers can write: + * @example + * import { iconv, secretmanager, idp, workflow, file } from "@tailor-platform/sdk/runtime"; + * + * const utf8 = iconv.convert(sjisBuffer, "Shift_JIS", "UTF-8"); + * const secret = await secretmanager.getSecret("my-vault", "API_KEY"); + * const client = new idp.Client({ namespace: "my-namespace" }); + * + * Importing this module also makes the global `tailor.*` / `tailordb` types + * available, so existing code that calls `tailor.iconv.convert(...)` directly + * continues to type-check without any additional `@tailor-platform/sdk/runtime/globals` + * import. + */ + +// Re-export the sentinel from globals so the bundler retains the +// `declare global` chunk in the emitted `.d.mts`. Importing this entry +// therefore activates the ambient `tailor.*` / `tailordb` types without +// any additional `@tailor-platform/sdk/runtime/globals` import. +export { __TAILOR_RUNTIME_GLOBALS_LOADED__ } from "./globals"; + +export * as iconv from "./iconv"; +export * as secretmanager from "./secretmanager"; +export * as authconnection from "./authconnection"; +export * as idp from "./idp"; +export * as workflow from "./workflow"; +export * as context from "./context"; +export * as file from "./file"; diff --git a/packages/sdk/src/runtime/secretmanager.ts b/packages/sdk/src/runtime/secretmanager.ts new file mode 100644 index 000000000..1cf0ca109 --- /dev/null +++ b/packages/sdk/src/runtime/secretmanager.ts @@ -0,0 +1,38 @@ +/** + * Secret manager utilities. + * + * Thin typed wrapper around the platform-provided `tailor.secretmanager` runtime API. + * At runtime this delegates to `globalThis.tailor.secretmanager`. Use + * `secretmanagerMock` from `@tailor-platform/sdk/vitest` to mock these calls + * in unit tests. + * @example + * import { secretmanager } from "@tailor-platform/sdk/runtime"; + * + * const apiKey = await secretmanager.getSecret("my-vault", "API_KEY"); + * const all = await secretmanager.getSecrets("my-vault", ["A", "B"] as const); + */ + +import "./globals"; + +/** + * Returns multiple secrets from a vault. Missing names are omitted from the result. + * @param vault - Vault name + * @param names - Secret names to fetch (use `as const` to narrow the result key) + * @returns Partial record keyed by the requested names + */ +export function getSecrets( + vault: string, + names: T, +): Promise>> { + return tailor.secretmanager.getSecrets(vault, names); +} + +/** + * Returns a single secret from a vault, or `undefined` when missing. + * @param vault - Vault name + * @param name - Secret name + * @returns The secret value, or `undefined` if not present + */ +export function getSecret(vault: string, name: string): Promise { + return tailor.secretmanager.getSecret(vault, name); +} diff --git a/packages/sdk/src/runtime/workflow.ts b/packages/sdk/src/runtime/workflow.ts new file mode 100644 index 000000000..665207967 --- /dev/null +++ b/packages/sdk/src/runtime/workflow.ts @@ -0,0 +1,76 @@ +/** + * Workflow utilities. + * + * Thin typed wrapper around the platform-provided `tailor.workflow` runtime API. + * At runtime this delegates to `globalThis.tailor.workflow`. Use `workflowMock` + * from `@tailor-platform/sdk/vitest` to mock these calls in unit tests. + * @example + * import { workflow } from "@tailor-platform/sdk/runtime"; + * + * const executionId = await workflow.triggerWorkflow("myWorkflow", { data: "value" }); + */ + +import "./globals"; + +/** {@link triggerWorkflow} option type. */ +export type AuthInvoker = tailor.workflow.AuthInvoker; + +/** {@link triggerWorkflow} option bag. */ +export type TriggerWorkflowOptions = tailor.workflow.TriggerWorkflowOptions; + +/** + * Triggers a workflow and returns its execution ID. + * @param workflow_name - Workflow name as defined in tailor.config + * @param args - Arguments forwarded to the workflow's main job + * @param options - Optional trigger options (e.g. `authInvoker`) + * @returns The execution ID of the triggered workflow + */ +export function triggerWorkflow( + workflow_name: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args?: any, + options?: TriggerWorkflowOptions, +): Promise { + return tailor.workflow.triggerWorkflow(workflow_name, args, options); +} + +/** + * Triggers a job function and returns its result. + * + * The TypeScript signature returns `any` to mirror the platform contract; the + * underlying call is synchronous on the server but `Promise`-based in the API. + * @param job_name - Job name as defined in the workflow + * @param args - Arguments forwarded to the job + * @returns The job's return value + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function triggerJobFunction(job_name: string, args?: any): any { + return tailor.workflow.triggerJobFunction(job_name, args); +} + +/** + * Suspends the current workflow execution and waits for an external signal to resume. + * @param key - Wait point key + * @param payload - Optional payload to record with the wait point + * @returns The payload supplied by the corresponding `resolve` call + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function wait(key: string, payload?: any): any { + return tailor.workflow.wait(key, payload); +} + +/** + * Resolves a waiting workflow execution, causing it to resume. + * @param executionId - The execution to resume + * @param key - Wait point key to resolve + * @param callback - Callback receiving the wait payload; its return value is forwarded to `wait` + * @returns A promise that resolves once the resolve has been recorded + */ +export function resolve( + executionId: string, + key: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback: (waitPayload: any) => any, +): Promise { + return tailor.workflow.resolve(executionId, key, callback); +} diff --git a/packages/sdk/src/vitest/__tests__/mock-types.test.ts b/packages/sdk/src/vitest/__tests__/mock-types.test.ts index bcd7b39b9..05f982bab 100644 --- a/packages/sdk/src/vitest/__tests__/mock-types.test.ts +++ b/packages/sdk/src/vitest/__tests__/mock-types.test.ts @@ -1,18 +1,19 @@ /** * Type-level tests verifying that the mock-injected globals expose the - * concrete signatures declared by `@tailor-platform/function-types`. + * concrete signatures declared by `@tailor-platform/sdk/runtime/globals`. * * Each test asserts a concrete return type (or call shape) — bare * `expectTypeOf(x).toEqualTypeOf()` self-comparisons are tautological * and are intentionally omitted because they would always pass. */ +import "@/runtime/globals"; import { afterAll, beforeAll, describe, expectTypeOf, test } from "vitest"; import { injectMocks, cleanupMocks } from "../mock"; beforeAll(() => injectMocks(globalThis)); afterAll(() => cleanupMocks(globalThis)); -describe("mock types match @tailor-platform/function-types", () => { +describe("mock types match @tailor-platform/sdk/runtime/globals", () => { describe("tailor.secretmanager", () => { test("getSecrets returns Promise>>", () => { expectTypeOf(tailor.secretmanager.getSecrets("vault", ["a", "b"] as const)).toEqualTypeOf< diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index e0fa2df81..fe0497ed1 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -9,8 +9,7 @@ "paths": { "@/*": ["./src/*"], "@tailor-proto/*": ["../tailor-proto/src/*"] - }, - "types": ["@tailor-platform/function-types"] + } }, "include": [ "./src/**/*.ts", diff --git a/packages/sdk/tsdown.config.ts b/packages/sdk/tsdown.config.ts index 8145204a0..c38d8f760 100644 --- a/packages/sdk/tsdown.config.ts +++ b/packages/sdk/tsdown.config.ts @@ -30,6 +30,8 @@ export default defineConfig({ "src/vitest/index.ts", "src/vitest/environment.ts", "src/vitest/setup.ts", + "src/runtime/index.ts", + "src/runtime/globals.ts", ], format: ["esm"], target: "node22", @@ -43,9 +45,6 @@ export default defineConfig({ js: ".mjs", dts: ".d.mts", }), - banner: { - dts: '/// ', - }, external: ["vite", "vitest"], // peer dependencies: prevent bundling, resolve at runtime sourcemap: true, plugins: [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d118082ce..efaf99fbe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -554,9 +554,6 @@ importers: '@tailor-platform/function-kysely-tailordb': specifier: 0.1.3 version: 0.1.3(kysely@0.28.16) - '@tailor-platform/function-types': - specifier: 0.8.5 - version: 0.8.5 '@toiroakr/lines-db': specifier: 0.9.2 version: 0.9.2(valibot@1.1.0(typescript@5.9.3)) @@ -2725,9 +2722,6 @@ packages: peerDependencies: kysely: '>= 0.24.0 < 1' - '@tailor-platform/function-types@0.8.5': - resolution: {integrity: sha512-D+6ylw2QHtms0mFLsfLCyk3Jeb8kXk2Vb8OqkzHCR8hS7LAna77W2ppHTioCmFq0xr+2ypqNByIyQKJB0+bqOQ==} - '@toiroakr/lines-db@0.9.2': resolution: {integrity: sha512-8ilJFrDcz1aaQb6inA6cQXovu4F3wRHFvG2sNXYlbtwyOg8csJTSB/BP6EEfvWRqt8j/uRK71rI99baS9yKYvw==} hasBin: true @@ -7122,8 +7116,6 @@ snapshots: dependencies: kysely: 0.28.16 - '@tailor-platform/function-types@0.8.5': {} - '@toiroakr/lines-db@0.9.2(valibot@1.1.0(typescript@5.9.3))': dependencies: '@standard-schema/spec': 1.1.0 From e1aedd20caad492e02520e7a1659abd8dd55d310 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 7 May 2026 15:57:20 +0900 Subject: [PATCH 02/35] refactor(sdk): expose runtime types without ambient globals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure src/runtime/globals.ts so every shape (interfaces, response types, error class) is defined as a top-level exported type. The `declare global` block now aliases those module-scope types into the `tailor.*` / `Tailordb` namespaces. As a result, the bundled `dist/runtime/*.d.mts` and `dist/vitest/index.d.mts` no longer reference `tailor.*` types — consumers can import and use the runtime wrappers without activating any ambient globals. Importing `@tailor-platform/sdk/runtime` no longer auto-activates the ambient declarations either. Users who still want unqualified `tailor.iconv.convert(...)` calls can opt in via a side-effect import of `@tailor-platform/sdk/runtime/globals` or by listing it in tsconfig `compilerOptions.types`. Also introduce `contextMock` so tests can configure invokers without spying on `globalThis.tailor.context.getInvoker`. Co-Authored-By: Claude Opus 4.7 --- .changeset/runtime-wrapper.md | 4 +- example/generated/files.ts | 18 +- example/tests/bundled_execution.test.ts | 5 +- .../generators/src/generated/files.ts | 18 +- packages/sdk/docs/runtime.md | 12 +- packages/sdk/package.json | 35 + packages/sdk/src/configure/index.ts | 10 - .../builtin/file-utils/generate-file-utils.ts | 22 +- packages/sdk/src/runtime/context.ts | 5 +- packages/sdk/src/runtime/file.ts | 39 +- packages/sdk/src/runtime/globals.ts | 607 +++++++++--------- packages/sdk/src/runtime/idp.ts | 44 +- packages/sdk/src/runtime/index.ts | 15 +- packages/sdk/src/runtime/workflow.ts | 5 +- packages/sdk/src/vitest/index.ts | 1 + packages/sdk/src/vitest/mock.ts | 58 +- packages/sdk/tsdown.config.ts | 7 + 17 files changed, 508 insertions(+), 397 deletions(-) diff --git a/.changeset/runtime-wrapper.md b/.changeset/runtime-wrapper.md index e71b62eb6..37653a3da 100644 --- a/.changeset/runtime-wrapper.md +++ b/.changeset/runtime-wrapper.md @@ -2,7 +2,7 @@ "@tailor-platform/sdk": minor --- -Add `@tailor-platform/sdk/runtime` — typed wrappers for the Tailor Platform Function runtime APIs (`tailor.iconv`, `tailor.secretmanager`, `tailor.authconnection`, `tailor.idp`, `tailor.workflow`, `tailor.context`, and `tailordb.file`). Importing the entry also activates the corresponding ambient `tailor.*` / `tailordb` global types, so existing code that calls `tailor.iconv.convert(...)` directly continues to type-check. +Add `@tailor-platform/sdk/runtime` — typed wrappers for the Tailor Platform Function runtime APIs (`tailor.iconv`, `tailor.secretmanager`, `tailor.authconnection`, `tailor.idp`, `tailor.workflow`, `tailor.context`, and `tailordb.file`). The wrappers and their types are fully self-contained, so you can use them without activating any ambient globals. ```ts import { iconv, secretmanager, idp, file } from "@tailor-platform/sdk/runtime"; @@ -13,4 +13,4 @@ const client = new idp.Client({ namespace: "my-namespace" }); const { metadata } = await file.upload("ns", "Document", "attachment", recordId, bytes); ``` -The SDK no longer depends on the external `@tailor-platform/function-types` package; its declarations are now vendored inside the SDK and exported as `@tailor-platform/sdk/runtime/globals` for projects that prefer to pin global types via `tsconfig.json`'s `compilerOptions.types`. Most users do not need to import `/runtime/globals` directly — `@tailor-platform/sdk/runtime` activates the ambient types as a side effect. +The SDK no longer depends on the external `@tailor-platform/function-types` package; its declarations are now vendored inside the SDK. If you still want unqualified `tailor.iconv.convert(...)` / `new tailordb.Client(...)` calls to type-check, opt into the globals by adding a side-effect `import "@tailor-platform/sdk/runtime/globals"` or by listing it in `tsconfig.json`'s `compilerOptions.types`. diff --git a/example/generated/files.ts b/example/generated/files.ts index 052806afd..0389b9acd 100644 --- a/example/generated/files.ts +++ b/example/generated/files.ts @@ -1,3 +1,11 @@ +import * as file from "@tailor-platform/sdk/runtime/file"; +import type { + FileUploadOptions, + FileUploadResponse, + FileMetadata, + FileStreamIterator, +} from "@tailor-platform/sdk/runtime/file"; + export interface TypeWithFiles { SalesOrder: { fields: "receipt" | "form"; @@ -21,7 +29,7 @@ export async function downloadFile( field: TypeWithFiles[T]["fields"], recordId: string, ) { - return await tailordb.file.download(namespaces[type], type, field, recordId); + return await file.download(namespaces[type], type, field, recordId); } export async function uploadFile( @@ -31,7 +39,7 @@ export async function uploadFile( data: string | ArrayBuffer | Uint8Array | number[], options?: FileUploadOptions, ): Promise { - return await tailordb.file.upload(namespaces[type], type, field, recordId, data, options); + return await file.upload(namespaces[type], type, field, recordId, data, options); } export async function deleteFile( @@ -39,7 +47,7 @@ export async function deleteFile( field: TypeWithFiles[T]["fields"], recordId: string, ): Promise { - return await tailordb.file.delete(namespaces[type], type, field, recordId); + return await file.delete(namespaces[type], type, field, recordId); } export async function getFileMetadata( @@ -47,7 +55,7 @@ export async function getFileMetadata( field: TypeWithFiles[T]["fields"], recordId: string, ): Promise { - return await tailordb.file.getMetadata(namespaces[type], type, field, recordId); + return await file.getMetadata(namespaces[type], type, field, recordId); } export async function openFileDownloadStream( @@ -55,5 +63,5 @@ export async function openFileDownloadStream( field: TypeWithFiles[T]["fields"], recordId: string, ): Promise { - return await tailordb.file.openDownloadStream(namespaces[type], type, field, recordId); + return await file.openDownloadStream(namespaces[type], type, field, recordId); } diff --git a/example/tests/bundled_execution.test.ts b/example/tests/bundled_execution.test.ts index 864b17239..cf7c69abf 100644 --- a/example/tests/bundled_execution.test.ts +++ b/example/tests/bundled_execution.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; -import { tailordbMock, workflowMock } from "@tailor-platform/sdk/vitest"; +import { contextMock, tailordbMock, workflowMock } from "@tailor-platform/sdk/vitest"; import { format as formatDate } from "date-fns"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; @@ -40,6 +40,7 @@ describe("bundled execution tests", () => { beforeEach(() => { tailordbMock.reset(); workflowMock.reset(); + contextMock.reset(); }); afterEach(() => { @@ -87,7 +88,7 @@ describe("bundled execution tests", () => { }); test("resolvers/showUserInfo.js returns user and invoker information", async () => { - vi.spyOn(globalThis.tailor.context, "getInvoker").mockReturnValue({ + contextMock.setInvoker({ id: "f1e2d3c4-b5a6-4798-89a0-1b2c3d4e5f60", type: "machine_user", workspaceId: "b39bdd61-d442-4a4e-8599-33a78a4e19ab", diff --git a/packages/create-sdk/templates/generators/src/generated/files.ts b/packages/create-sdk/templates/generators/src/generated/files.ts index 8bff0ff81..e7498a317 100644 --- a/packages/create-sdk/templates/generators/src/generated/files.ts +++ b/packages/create-sdk/templates/generators/src/generated/files.ts @@ -1,3 +1,11 @@ +import * as file from "@tailor-platform/sdk/runtime/file"; +import type { + FileUploadOptions, + FileUploadResponse, + FileMetadata, + FileStreamIterator, +} from "@tailor-platform/sdk/runtime/file"; + export interface TypeWithFiles { Product: { fields: "image"; @@ -13,7 +21,7 @@ export async function downloadFile( field: TypeWithFiles[T]["fields"], recordId: string, ) { - return await tailordb.file.download(namespaces[type], type, field, recordId); + return await file.download(namespaces[type], type, field, recordId); } export async function uploadFile( @@ -23,7 +31,7 @@ export async function uploadFile( data: string | ArrayBuffer | Uint8Array | number[], options?: FileUploadOptions, ): Promise { - return await tailordb.file.upload(namespaces[type], type, field, recordId, data, options); + return await file.upload(namespaces[type], type, field, recordId, data, options); } export async function deleteFile( @@ -31,7 +39,7 @@ export async function deleteFile( field: TypeWithFiles[T]["fields"], recordId: string, ): Promise { - return await tailordb.file.delete(namespaces[type], type, field, recordId); + return await file.delete(namespaces[type], type, field, recordId); } export async function getFileMetadata( @@ -39,7 +47,7 @@ export async function getFileMetadata( field: TypeWithFiles[T]["fields"], recordId: string, ): Promise { - return await tailordb.file.getMetadata(namespaces[type], type, field, recordId); + return await file.getMetadata(namespaces[type], type, field, recordId); } export async function openFileDownloadStream( @@ -47,5 +55,5 @@ export async function openFileDownloadStream( field: TypeWithFiles[T]["fields"], recordId: string, ): Promise { - return await tailordb.file.openDownloadStream(namespaces[type], type, field, recordId); + return await file.openDownloadStream(namespaces[type], type, field, recordId); } diff --git a/packages/sdk/docs/runtime.md b/packages/sdk/docs/runtime.md index eb3d84d37..d45215bf4 100644 --- a/packages/sdk/docs/runtime.md +++ b/packages/sdk/docs/runtime.md @@ -6,7 +6,7 @@ - Get IDE-friendly imports (`iconv.convert`, `idp.Client`, …) instead of unmemorable `tailor.iconv.convert(...)` calls - Use the same module surface in resolvers, executors, and workflows -Importing this module also activates the ambient `tailor.*` / `tailordb` global types as a side effect — code that calls `tailor.iconv.convert(...)` directly continues to type-check. +The wrappers and their associated types are self-contained — you do not need to activate any ambient globals to use them. If you also want `tailor.iconv.convert(...)` calls to type-check, opt into the globals via the [Activating the global types](#activating-the-global-types) section below. ## Quick Start @@ -50,9 +50,15 @@ import type { ListUsersResponse, ClientConfig } from "@tailor-platform/sdk/runti ## Activating the global types -Most users do not need to touch the globals entry. Importing `@tailor-platform/sdk/runtime` once anywhere in your project is enough — the side-effect import wires up the `declare global { … }` block so that calls like `tailor.iconv.convert(...)` and `new tailordb.Client(...)` type-check from any other file. +Most users do not need to touch the globals entry — `@tailor-platform/sdk/runtime` (and its subpath modules) cover the same surface without depending on any ambient declaration. -If you prefer to enable the globals without an `import`, register them in `tsconfig.json`: +If you do want unqualified calls like `tailor.iconv.convert(...)` and `new tailordb.Client(...)` to type-check, opt in by adding a single side-effect import anywhere in your project: + +```ts +import "@tailor-platform/sdk/runtime/globals"; +``` + +Or register the entry in `tsconfig.json`: ```jsonc { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index c7d9c6e36..75a598c88 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -93,6 +93,41 @@ "types": "./dist/runtime/globals.d.mts", "import": "./dist/runtime/globals.mjs", "default": "./dist/runtime/globals.mjs" + }, + "./runtime/iconv": { + "types": "./dist/runtime/iconv.d.mts", + "import": "./dist/runtime/iconv.mjs", + "default": "./dist/runtime/iconv.mjs" + }, + "./runtime/secretmanager": { + "types": "./dist/runtime/secretmanager.d.mts", + "import": "./dist/runtime/secretmanager.mjs", + "default": "./dist/runtime/secretmanager.mjs" + }, + "./runtime/authconnection": { + "types": "./dist/runtime/authconnection.d.mts", + "import": "./dist/runtime/authconnection.mjs", + "default": "./dist/runtime/authconnection.mjs" + }, + "./runtime/idp": { + "types": "./dist/runtime/idp.d.mts", + "import": "./dist/runtime/idp.mjs", + "default": "./dist/runtime/idp.mjs" + }, + "./runtime/workflow": { + "types": "./dist/runtime/workflow.d.mts", + "import": "./dist/runtime/workflow.mjs", + "default": "./dist/runtime/workflow.mjs" + }, + "./runtime/context": { + "types": "./dist/runtime/context.d.mts", + "import": "./dist/runtime/context.mjs", + "default": "./dist/runtime/context.mjs" + }, + "./runtime/file": { + "types": "./dist/runtime/file.d.mts", + "import": "./dist/runtime/file.mjs", + "default": "./dist/runtime/file.mjs" } }, "scripts": { diff --git a/packages/sdk/src/configure/index.ts b/packages/sdk/src/configure/index.ts index 35d345b44..9147c5dcc 100644 --- a/packages/sdk/src/configure/index.ts +++ b/packages/sdk/src/configure/index.ts @@ -1,16 +1,6 @@ import { t as _t } from "@/configure/types"; import type * as helperTypes from "@/types/helpers"; -/** - * Re-exported so the bundled `.d.mts` keeps a value-level reference to - * `@/runtime/globals`, which forces the rolldown dts emitter to include the - * vendored ambient `tailor.*` / `tailordb` declarations alongside the SDK - * main entry. Importing anything from `@tailor-platform/sdk` therefore - * activates those globals automatically. - * @internal - */ -export { __TAILOR_RUNTIME_GLOBALS_LOADED__ } from "@/runtime/globals"; - type TailorOutput = helperTypes.output; export type infer = TailorOutput; diff --git a/packages/sdk/src/plugin/builtin/file-utils/generate-file-utils.ts b/packages/sdk/src/plugin/builtin/file-utils/generate-file-utils.ts index 7144ab294..4d5ea1be8 100644 --- a/packages/sdk/src/plugin/builtin/file-utils/generate-file-utils.ts +++ b/packages/sdk/src/plugin/builtin/file-utils/generate-file-utils.ts @@ -36,6 +36,17 @@ export function generateUnifiedFileUtils( }) .join("\n"); + const importStatement = + multiline /* ts */ ` + import * as file from "@tailor-platform/sdk/runtime/file"; + import type { + FileUploadOptions, + FileUploadResponse, + FileMetadata, + FileStreamIterator, + } from "@tailor-platform/sdk/runtime/file"; + ` + "\n"; + const interfaceDefinition = multiline /* ts */ ` export interface TypeWithFiles { @@ -63,7 +74,7 @@ export function generateUnifiedFileUtils( field: TypeWithFiles[T]["fields"], recordId: string, ) { - return await tailordb.file.download(namespaces[type], type, field, recordId); + return await file.download(namespaces[type], type, field, recordId); } ` + "\n"; @@ -77,7 +88,7 @@ export function generateUnifiedFileUtils( data: string | ArrayBuffer | Uint8Array | number[], options?: FileUploadOptions, ): Promise { - return await tailordb.file.upload(namespaces[type], type, field, recordId, data, options); + return await file.upload(namespaces[type], type, field, recordId, data, options); } ` + "\n"; @@ -89,7 +100,7 @@ export function generateUnifiedFileUtils( field: TypeWithFiles[T]["fields"], recordId: string, ): Promise { - return await tailordb.file.delete(namespaces[type], type, field, recordId); + return await file.delete(namespaces[type], type, field, recordId); } ` + "\n"; @@ -101,7 +112,7 @@ export function generateUnifiedFileUtils( field: TypeWithFiles[T]["fields"], recordId: string, ): Promise { - return await tailordb.file.getMetadata(namespaces[type], type, field, recordId); + return await file.getMetadata(namespaces[type], type, field, recordId); } ` + "\n"; @@ -113,11 +124,12 @@ export function generateUnifiedFileUtils( field: TypeWithFiles[T]["fields"], recordId: string, ): Promise { - return await tailordb.file.openDownloadStream(namespaces[type], type, field, recordId); + return await file.openDownloadStream(namespaces[type], type, field, recordId); } ` + "\n"; return [ + importStatement, interfaceDefinition, namespacesDefinition, downloadFunction, diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index 1456a3825..c4f9cf982 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -14,9 +14,10 @@ */ import "./globals"; +import type { ContextInvoker } from "./globals"; -/** Re-exported invoker type from the global runtime. */ -export type Invoker = tailor.context.Invoker; +/** Information about the invoker of the current function execution. */ +export type Invoker = ContextInvoker; /** * Returns information about the invoker of the current function execution, diff --git a/packages/sdk/src/runtime/file.ts b/packages/sdk/src/runtime/file.ts index 6a6223daf..3ba7e8ce8 100644 --- a/packages/sdk/src/runtime/file.ts +++ b/packages/sdk/src/runtime/file.ts @@ -17,18 +17,35 @@ */ import "./globals"; +import type { + UploadMetadata, + DownloadMetadata, + FileMetadata, + StreamMetadata, + FileUploadOptions, + FileUploadResponse, + FileDownloadResponse, + FileDownloadAsBase64Response, + StreamValue, + FileStreamIterator, + TailorDBFileError, + TailorDBFileErrorCode, +} from "./globals"; -export type UploadMetadata = globalThis.UploadMetadata; -export type DownloadMetadata = globalThis.DownloadMetadata; -export type FileMetadata = globalThis.FileMetadata; -export type StreamMetadata = globalThis.StreamMetadata; -export type FileUploadOptions = globalThis.FileUploadOptions; -export type FileUploadResponse = globalThis.FileUploadResponse; -export type FileDownloadResponse = globalThis.FileDownloadResponse; -export type FileDownloadAsBase64Response = globalThis.FileDownloadAsBase64Response; -export type StreamValue = globalThis.StreamValue; -export type FileStreamIterator = globalThis.FileStreamIterator; -export type TailorDBFileError = globalThis.TailorDBFileError; +export type { + UploadMetadata, + DownloadMetadata, + FileMetadata, + StreamMetadata, + FileUploadOptions, + FileUploadResponse, + FileDownloadResponse, + FileDownloadAsBase64Response, + StreamValue, + FileStreamIterator, + TailorDBFileError, + TailorDBFileErrorCode, +}; /** * Upload a file to TailorDB. diff --git a/packages/sdk/src/runtime/globals.ts b/packages/sdk/src/runtime/globals.ts index 12a395f63..5f5a6e11e 100644 --- a/packages/sdk/src/runtime/globals.ts +++ b/packages/sdk/src/runtime/globals.ts @@ -16,7 +16,286 @@ * exposes typed wrappers that cover the same surface without relying on globals. */ -/* eslint-disable @typescript-eslint/no-namespace, jsdoc/require-param, jsdoc/require-returns, jsdoc/require-param-description */ +/* eslint-disable @typescript-eslint/no-namespace, @typescript-eslint/no-explicit-any, jsdoc/require-param, jsdoc/require-returns, jsdoc/require-param-description */ + +// --------------------------------------------------------------------------- +// Module-scope types (exported, non-global) +// +// These types describe the data shapes used by the platform runtime. The +// `declare global` block below aliases each of them into the appropriate +// `tailor.*` / global namespace, so callers who opt into the globals see the +// same surface they always did. Callers who do not opt in can still import +// these types directly via `@tailor-platform/sdk/runtime/*` — none of the +// types below reference globals, so they are self-contained. +// --------------------------------------------------------------------------- + +// --- Tailordb ------------------------------------------------------------- + +/** Result of a single `queryObject` call against the TailorDB driver. */ +export interface TailordbQueryResult { + rows: T[]; + command: TailordbCommandType; + rowCount: number; +} + +/** SQL command type recorded on a {@link TailordbQueryResult}. */ +export type TailordbCommandType = + | "INSERT" + | "DELETE" + | "UPDATE" + | "SELECT" + | "MOVE" + | "FETCH" + | "COPY" + | "CREATE"; + +// --- TailorDB file API --------------------------------------------------- + +/** Upload response metadata. */ +export interface UploadMetadata { + fileSize: number; + sha256sum: string; +} + +/** Download response metadata. */ +export interface DownloadMetadata { + contentType: string; + fileSize: number; + sha256sum: string; + lastUploadedAt: string; +} + +/** File metadata (for `getMetadata`). */ +export interface FileMetadata { + contentType: string; + fileSize: number; + sha256sum: string; + urlPath: string; + lastUploadedAt?: string; +} + +/** Stream metadata (first chunk). */ +export interface StreamMetadata { + contentType: string; + fileSize: number; + sha256sum: string; +} + +/** Upload options. */ +export interface FileUploadOptions { + contentType?: string; +} + +/** Upload response. */ +export interface FileUploadResponse { + metadata: UploadMetadata; +} + +/** Download response. */ +export interface FileDownloadResponse { + data: Uint8Array; + metadata: DownloadMetadata; +} + +/** Download-as-Base64 response. */ +export interface FileDownloadAsBase64Response { + data: string; + metadata: DownloadMetadata; +} + +/** Stream chunk types. */ +export type StreamValue = + | { type: "metadata"; metadata: StreamMetadata } + | { type: "chunk"; data: Uint8Array; position: number } + | { type: "complete" }; + +/** Stream iterator interface. */ +export interface FileStreamIterator extends AsyncIterableIterator { + next(): Promise>; + close(): Promise; +} + +/** TailorDB File API surface. */ +export interface TailorDBFileAPI { + upload( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + data: string | ArrayBuffer | Uint8Array | number[], + options?: FileUploadOptions, + ): Promise; + + download( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + ): Promise; + + downloadAsBase64( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + ): Promise; + + delete(namespace: string, typeName: string, fieldName: string, recordId: string): Promise; + + getMetadata( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + ): Promise; + + openDownloadStream( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + ): Promise; +} + +/** Error code emitted by `TailorDBFileError`. */ +export type TailorDBFileErrorCode = + | "INVALID_PARAMS" + | "INVALID_DATA_TYPE" + | "OPERATION_FAILED" + | "DELETE_FAILED" + | "STREAM_OPEN_FAILED" + | "STREAM_READ_ERROR" + | "STREAM_ERROR" + | "FILE_TOO_LARGE"; + +/** + * Type-only shape of the `TailorDBFileError` runtime class. The class itself + * is declared globally below; this interface mirrors it so callers can use + * `import type { TailorDBFileError } from "@tailor-platform/sdk/runtime/file"` + * without depending on the global declaration. + */ +export interface TailorDBFileError extends Error { + name: "TailorDBFileError"; + code?: TailorDBFileErrorCode; + cause?: unknown; +} + +// --- tailor.idp ----------------------------------------------------------- + +/** Configuration for creating an IDP Client. */ +export interface IdpClientConfig { + namespace: string; +} + +/** User object returned from IDP operations. */ +export interface IdpUser { + id: string; + name: string; + disabled: boolean; + createdAt?: string; + updatedAt?: string; +} + +/** Query options for filtering users. */ +export interface IdpUserQuery { + /** Filter by user IDs */ + ids?: string[]; + /** Filter by user names */ + names?: string[]; +} + +/** Options for listing users. */ +export interface IdpListUsersOptions { + /** Maximum number of users to return */ + first?: number; + /** Page token for pagination */ + after?: string; + /** Query filter for users */ + query?: IdpUserQuery; +} + +/** Response from listing users. */ +export interface IdpListUsersResponse { + users: IdpUser[]; + nextPageToken: string | null; + totalCount: number; +} + +/** Input for creating a new user. */ +export interface IdpCreateUserInput { + /** The user's name (typically email) */ + name: string; + /** The user's password. If omitted, the user is created without a password (cannot log in with any password). */ + password?: string; + /** Whether the user is disabled */ + disabled?: boolean; +} + +/** Input for updating an existing user. */ +export interface IdpUpdateUserInput { + /** The user's ID */ + id: string; + /** New name for the user */ + name?: string; + /** New password for the user. Cannot be used with clearPassword. */ + password?: string; + /** If true, remove the user's password. Cannot be used with password. */ + clearPassword?: boolean; + /** New disabled status for the user */ + disabled?: boolean; +} + +/** Input for sending a password reset email. */ +export interface IdpSendPasswordResetEmailInput { + /** The ID of the user */ + userId: string; + /** The URI to redirect to after password reset */ + redirectUri: string; + /** The sender display name. Defaults to 'Tailor Platform IdP'. */ + fromName?: string; + /** The email subject line. Defaults to the localized default subject. */ + subject?: string; +} + +// --- tailor.workflow ----------------------------------------------------- + +/** + * Specifies the machine user that should be used to execute the workflow. + * This allows workflows to run with specific authentication context. + */ +export interface WorkflowAuthInvoker { + /** The namespace where the machine user is defined */ + namespace: string; + /** The name of the machine user to use for workflow execution */ + machineUserName: string; +} + +/** Options for triggering a workflow. */ +export interface WorkflowTriggerWorkflowOptions { + /** Optional authentication invoker to specify which machine user should execute the workflow */ + authInvoker?: WorkflowAuthInvoker; +} + +// --- tailor.context ------------------------------------------------------- + +/** Information about the invoker of the current function execution. */ +export interface ContextInvoker { + /** The invoker's ID */ + id: string; + /** The invoker's type */ + type: "user" | "machine_user"; + /** The workspace ID */ + workspaceId: string; + /** The invoker's attribute IDs */ + attributes: string[]; + /** The invoker's attribute map */ + attributeMap: Record; +} + +// --------------------------------------------------------------------------- +// Ambient globals — alias the module-scope types into the runtime namespaces +// --------------------------------------------------------------------------- + declare global { namespace Tailordb { class Client { @@ -26,21 +305,8 @@ declare global { queryObject(sql: string, args?: readonly unknown[]): Promise>; } - interface QueryResult { - rows: T[]; - command: CommandType; - rowCount: number; - } - - type CommandType = - | "INSERT" - | "DELETE" - | "UPDATE" - | "SELECT" - | "MOVE" - | "FETCH" - | "COPY" - | "CREATE"; + type QueryResult = TailordbQueryResult; + type CommandType = TailordbCommandType; } // eslint-disable-next-line no-var @@ -78,7 +344,6 @@ declare global { * getConnectionToken returns the access token for an auth connection * @param connectionName */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any function getConnectionToken(connectionName: string): Promise; } @@ -138,267 +403,22 @@ declare global { } } - // TailorDB File Extension Types - - /** - * Custom error class for TailorDB File operations - */ + /** Custom error class for TailorDB File operations. */ class TailorDBFileError extends Error { name: "TailorDBFileError"; - code?: - | "INVALID_PARAMS" - | "INVALID_DATA_TYPE" - | "OPERATION_FAILED" - | "DELETE_FAILED" - | "STREAM_OPEN_FAILED" - | "STREAM_READ_ERROR" - | "STREAM_ERROR" - | "FILE_TOO_LARGE"; + code?: TailorDBFileErrorCode; cause?: unknown; } - /** - * Upload response metadata - */ - interface UploadMetadata { - fileSize: number; - sha256sum: string; - } - - /** - * Download response metadata - */ - interface DownloadMetadata { - contentType: string; - fileSize: number; - sha256sum: string; - lastUploadedAt: string; - } - - /** - * File metadata (for getMetadata API) - */ - interface FileMetadata { - contentType: string; - fileSize: number; - sha256sum: string; - urlPath: string; - lastUploadedAt?: string; - } - - /** - * Stream metadata (first chunk) - */ - interface StreamMetadata { - contentType: string; - fileSize: number; - sha256sum: string; - } - - /** - * Upload options interface - */ - interface FileUploadOptions { - contentType?: string; - } - - /** - * Upload response interface - */ - interface FileUploadResponse { - metadata: UploadMetadata; - } - - /** - * Download response interface - */ - interface FileDownloadResponse { - data: Uint8Array; - metadata: DownloadMetadata; - } - - /** - * Download as Base64 response interface - */ - interface FileDownloadAsBase64Response { - data: string; - metadata: DownloadMetadata; - } - - /** - * Stream chunk types - */ - type StreamValue = - | { type: "metadata"; metadata: StreamMetadata } - | { type: "chunk"; data: Uint8Array; position: number } - | { type: "complete" }; - - /** - * Stream iterator interface - */ - interface FileStreamIterator extends AsyncIterableIterator { - next(): Promise>; - close(): Promise; - } - - /** - * TailorDB File API - */ - interface TailorDBFileAPI { - /** - * Upload a file to TailorDB - */ - upload( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - data: string | ArrayBuffer | Uint8Array | number[], - options?: FileUploadOptions, - ): Promise; - - /** - * Download a file from TailorDB - * @throws {TailorDBFileError} FILE_TOO_LARGE if file exceeds 10MB - use openDownloadStream() for large files - */ - download( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - ): Promise; - - /** - * Download a file from TailorDB as Base64 string. - * Unlike download which returns decoded binary data (Uint8Array), - * this returns the raw Base64-encoded string for use cases requiring - * Base64 format (e.g., embedding in JSON responses, data URIs). - * @throws {TailorDBFileError} FILE_TOO_LARGE if file exceeds 10MB - use openDownloadStream() for large files - */ - downloadAsBase64( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - ): Promise; - - /** - * Delete a file from TailorDB - */ - delete(namespace: string, typeName: string, fieldName: string, recordId: string): Promise; - - /** - * Get file metadata from TailorDB - */ - getMetadata( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - ): Promise; - - /** - * Open a download stream for large files - */ - openDownloadStream( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - ): Promise; - } - namespace tailor.idp { - /** - * Configuration for creating an IDP Client - */ - interface ClientConfig { - namespace: string; - } - - /** - * User object returned from IDP operations - */ - interface User { - id: string; - name: string; - disabled: boolean; - createdAt?: string; - updatedAt?: string; - } - - /** - * Query options for filtering users - */ - interface UserQuery { - /** Filter by user IDs */ - ids?: string[]; - /** Filter by user names */ - names?: string[]; - } - - /** - * Options for listing users - */ - interface ListUsersOptions { - /** Maximum number of users to return */ - first?: number; - /** Page token for pagination */ - after?: string; - /** Query filter for users */ - query?: UserQuery; - } - - /** - * Response from listing users - */ - interface ListUsersResponse { - users: User[]; - nextPageToken: string | null; - totalCount: number; - } - - /** - * Input for creating a new user - */ - interface CreateUserInput { - /** The user's name (typically email) */ - name: string; - /** The user's password. If omitted, the user is created without a password (cannot log in with any password). */ - password?: string; - /** Whether the user is disabled */ - disabled?: boolean; - } - - /** - * Input for updating an existing user - */ - interface UpdateUserInput { - /** The user's ID */ - id: string; - /** New name for the user */ - name?: string; - /** New password for the user. Cannot be used with clearPassword. */ - password?: string; - /** If true, remove the user's password. Cannot be used with password. */ - clearPassword?: boolean; - /** New disabled status for the user */ - disabled?: boolean; - } - - /** - * Input for sending a password reset email - */ - interface SendPasswordResetEmailInput { - /** The ID of the user */ - userId: string; - /** The URI to redirect to after password reset */ - redirectUri: string; - /** The sender display name. Defaults to 'Tailor Platform IdP'. */ - fromName?: string; - /** The email subject line. Defaults to the localized default subject. */ - subject?: string; - } + type ClientConfig = IdpClientConfig; + type User = IdpUser; + type UserQuery = IdpUserQuery; + type ListUsersOptions = IdpListUsersOptions; + type ListUsersResponse = IdpListUsersResponse; + type CreateUserInput = IdpCreateUserInput; + type UpdateUserInput = IdpUpdateUserInput; + type SendPasswordResetEmailInput = IdpSendPasswordResetEmailInput; /** * IDP Client for user management operations @@ -446,24 +466,8 @@ declare global { } namespace tailor.workflow { - /** - * Specifies the machine user that should be used to execute the workflow. - * This allows workflows to run with specific authentication context. - */ - interface AuthInvoker { - /** The namespace where the machine user is defined */ - namespace: string; - /** The name of the machine user to use for workflow execution */ - machineUserName: string; - } - - /** - * Options for triggering a workflow - */ - interface TriggerWorkflowOptions { - /** Optional authentication invoker to specify which machine user should execute the workflow */ - authInvoker?: AuthInvoker; - } + type AuthInvoker = WorkflowAuthInvoker; + type TriggerWorkflowOptions = WorkflowTriggerWorkflowOptions; /** * Triggers a workflow and returns its execution ID. @@ -473,7 +477,6 @@ declare global { */ function triggerWorkflow( workflow_name: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any args?: any, options?: TriggerWorkflowOptions, ): Promise; @@ -483,22 +486,17 @@ declare global { * @param job_name * @param args */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any function triggerJobFunction(job_name: string, args?: any): any; /** * Suspends the current workflow execution and waits for an external signal to resume. - * The workflow will be parked in "Waiting" status until resolved via `resolve()`. * @param key * @param payload */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any function wait(key: string, payload?: any): any; /** * Resolves a waiting workflow execution, causing it to resume. - * The callback receives the wait payload and must return a JSON-serializable result - * that will be passed back to the `wait()` caller. * @param executionId * @param key * @param callback @@ -506,27 +504,12 @@ declare global { function resolve( executionId: string, key: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any callback: (waitPayload: any) => any, ): Promise; } namespace tailor.context { - /** - * Information about the invoker of the current function execution. - */ - interface Invoker { - /** The invoker's ID */ - id: string; - /** The invoker's type */ - type: "user" | "machine_user"; - /** The workspace ID */ - workspaceId: string; - /** The invoker's attribute IDs */ - attributes: string[]; - /** The invoker's attribute map */ - attributeMap: Record; - } + type Invoker = ContextInvoker; /** * Returns information about the invoker of the current function execution, diff --git a/packages/sdk/src/runtime/idp.ts b/packages/sdk/src/runtime/idp.ts index 545504792..d994e04f1 100644 --- a/packages/sdk/src/runtime/idp.ts +++ b/packages/sdk/src/runtime/idp.ts @@ -12,30 +12,40 @@ */ import "./globals"; +import type { + IdpClientConfig, + IdpUser, + IdpUserQuery, + IdpListUsersOptions, + IdpListUsersResponse, + IdpCreateUserInput, + IdpUpdateUserInput, + IdpSendPasswordResetEmailInput, +} from "./globals"; /** Configuration object for {@link Client}. */ -export type ClientConfig = tailor.idp.ClientConfig; +export type ClientConfig = IdpClientConfig; /** User record returned by IDP operations. */ -export type User = tailor.idp.User; +export type User = IdpUser; /** Filter options for {@link Client.users}. */ -export type UserQuery = tailor.idp.UserQuery; +export type UserQuery = IdpUserQuery; /** Pagination/filter options for {@link Client.users}. */ -export type ListUsersOptions = tailor.idp.ListUsersOptions; +export type ListUsersOptions = IdpListUsersOptions; /** Response shape for {@link Client.users}. */ -export type ListUsersResponse = tailor.idp.ListUsersResponse; +export type ListUsersResponse = IdpListUsersResponse; /** Input for {@link Client.createUser}. */ -export type CreateUserInput = tailor.idp.CreateUserInput; +export type CreateUserInput = IdpCreateUserInput; /** Input for {@link Client.updateUser}. */ -export type UpdateUserInput = tailor.idp.UpdateUserInput; +export type UpdateUserInput = IdpUpdateUserInput; /** Input for {@link Client.sendPasswordResetEmail}. */ -export type SendPasswordResetEmailInput = tailor.idp.SendPasswordResetEmailInput; +export type SendPasswordResetEmailInput = IdpSendPasswordResetEmailInput; /** * IDP Client for user management operations. @@ -43,10 +53,10 @@ export type SendPasswordResetEmailInput = tailor.idp.SendPasswordResetEmailInput * Wraps the platform-provided `tailor.idp.Client` and exposes the same surface. */ export class Client { - private impl: tailor.idp.Client; + #impl: tailor.idp.Client; constructor(config: ClientConfig) { - this.impl = new tailor.idp.Client(config); + this.#impl = new tailor.idp.Client(config); } /** @@ -55,7 +65,7 @@ export class Client { * @returns Page of users with `nextPageToken` and `totalCount` */ users(options?: ListUsersOptions): Promise { - return this.impl.users(options); + return this.#impl.users(options); } /** @@ -64,7 +74,7 @@ export class Client { * @returns The matching user */ user(userId: string): Promise { - return this.impl.user(userId); + return this.#impl.user(userId); } /** @@ -73,7 +83,7 @@ export class Client { * @returns The matching user */ userByName(name: string): Promise { - return this.impl.userByName(name); + return this.#impl.userByName(name); } /** @@ -82,7 +92,7 @@ export class Client { * @returns The newly created user */ createUser(input: CreateUserInput): Promise { - return this.impl.createUser(input); + return this.#impl.createUser(input); } /** @@ -91,7 +101,7 @@ export class Client { * @returns The updated user */ updateUser(input: UpdateUserInput): Promise { - return this.impl.updateUser(input); + return this.#impl.updateUser(input); } /** @@ -100,7 +110,7 @@ export class Client { * @returns `true` when the user was deleted */ deleteUser(userId: string): Promise { - return this.impl.deleteUser(userId); + return this.#impl.deleteUser(userId); } /** @@ -109,6 +119,6 @@ export class Client { * @returns `true` when the email was queued */ sendPasswordResetEmail(input: SendPasswordResetEmailInput): Promise { - return this.impl.sendPasswordResetEmail(input); + return this.#impl.sendPasswordResetEmail(input); } } diff --git a/packages/sdk/src/runtime/index.ts b/packages/sdk/src/runtime/index.ts index 3f56bc307..0c2c44408 100644 --- a/packages/sdk/src/runtime/index.ts +++ b/packages/sdk/src/runtime/index.ts @@ -10,18 +10,13 @@ * const secret = await secretmanager.getSecret("my-vault", "API_KEY"); * const client = new idp.Client({ namespace: "my-namespace" }); * - * Importing this module also makes the global `tailor.*` / `tailordb` types - * available, so existing code that calls `tailor.iconv.convert(...)` directly - * continues to type-check without any additional `@tailor-platform/sdk/runtime/globals` - * import. + * Importing this entry does NOT activate the ambient `tailor.*` / `tailordb` + * global types — the wrappers and their associated types are self-contained. + * If you want to call `tailor.iconv.convert(...)` directly, add a side-effect + * import of `@tailor-platform/sdk/runtime/globals` (or list it in tsconfig + * `compilerOptions.types`). */ -// Re-export the sentinel from globals so the bundler retains the -// `declare global` chunk in the emitted `.d.mts`. Importing this entry -// therefore activates the ambient `tailor.*` / `tailordb` types without -// any additional `@tailor-platform/sdk/runtime/globals` import. -export { __TAILOR_RUNTIME_GLOBALS_LOADED__ } from "./globals"; - export * as iconv from "./iconv"; export * as secretmanager from "./secretmanager"; export * as authconnection from "./authconnection"; diff --git a/packages/sdk/src/runtime/workflow.ts b/packages/sdk/src/runtime/workflow.ts index 665207967..38750c31b 100644 --- a/packages/sdk/src/runtime/workflow.ts +++ b/packages/sdk/src/runtime/workflow.ts @@ -11,12 +11,13 @@ */ import "./globals"; +import type { WorkflowAuthInvoker, WorkflowTriggerWorkflowOptions } from "./globals"; /** {@link triggerWorkflow} option type. */ -export type AuthInvoker = tailor.workflow.AuthInvoker; +export type AuthInvoker = WorkflowAuthInvoker; /** {@link triggerWorkflow} option bag. */ -export type TriggerWorkflowOptions = tailor.workflow.TriggerWorkflowOptions; +export type TriggerWorkflowOptions = WorkflowTriggerWorkflowOptions; /** * Triggers a workflow and returns its execution ID. diff --git a/packages/sdk/src/vitest/index.ts b/packages/sdk/src/vitest/index.ts index 70a89b914..e88318f76 100644 --- a/packages/sdk/src/vitest/index.ts +++ b/packages/sdk/src/vitest/index.ts @@ -67,4 +67,5 @@ export { idpMock, fileMock, iconvMock, + contextMock, } from "./mock"; diff --git a/packages/sdk/src/vitest/mock.ts b/packages/sdk/src/vitest/mock.ts index 9cf492e4d..a07fea785 100644 --- a/packages/sdk/src/vitest/mock.ts +++ b/packages/sdk/src/vitest/mock.ts @@ -6,6 +6,9 @@ * responses and assert on recorded calls via the exported mock objects. */ +import "../runtime/globals"; +import type { ContextInvoker, IdpUser } from "../runtime/globals"; + // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- @@ -66,6 +69,10 @@ interface WorkflowCall { args: unknown[]; } +interface ContextCall { + method: "getInvoker"; +} + interface MockState { // TailorDB queryResolver: QueryResolver; @@ -96,6 +103,9 @@ interface MockState { // Iconv iconvResolver: IconvResolver | null; iconvCalls: IconvCall[]; + // Context + invoker: ContextInvoker | null; + contextCalls: ContextCall[]; } // --------------------------------------------------------------------------- @@ -143,6 +153,8 @@ function createDefaultState(): MockState { fileCalls: [], iconvResolver: null, iconvCalls: [], + invoker: null, + contextCalls: [], }; } @@ -337,6 +349,28 @@ export const workflowMock = { // SecretManager Mock // --------------------------------------------------------------------------- +/** Mock control for `tailor.context` — invoker store and call recording. */ +export const contextMock = { + /** + * Set the invoker returned by `tailor.context.getInvoker()`. Pass `null` to + * simulate an anonymous (unauthenticated) caller — the default. + * @param invoker - Invoker to return, or `null` for anonymous + */ + setInvoker(invoker: ContextInvoker | null): void { + getState().invoker = invoker; + }, + + get calls(): ContextCall[] { + return getState().contextCalls; + }, + + reset(): void { + const state = getState(); + state.invoker = null; + state.contextCalls.length = 0; + }, +}; + /** Mock control for `tailor.secretmanager` — secret store and call recording. */ export const secretmanagerMock = { setSecrets(secrets: Record>): void { @@ -602,8 +636,10 @@ async function mockResolve( // Mock: tailor.context // --------------------------------------------------------------------------- -function mockGetInvoker(): tailor.context.Invoker | null { - return null; +function mockGetInvoker(): ContextInvoker | null { + const state = getState(); + state.contextCalls.push({ method: "getInvoker" }); + return state.invoker; } // --------------------------------------------------------------------------- @@ -681,23 +717,23 @@ class MockIdpClient { first?: number; after?: string; query?: { ids?: string[]; names?: string[] }; - }): Promise<{ users: tailor.idp.User[]; nextPageToken: string | null; totalCount: number }> { + }): Promise<{ users: IdpUser[]; nextPageToken: string | null; totalCount: number }> { return resolveIdpCall("users", [options], this.#namespace) as Awaited< ReturnType >; } - async user(userId: string): Promise { - return resolveIdpCall("user", [userId], this.#namespace) as tailor.idp.User; + async user(userId: string): Promise { + return resolveIdpCall("user", [userId], this.#namespace) as IdpUser; } - async userByName(name: string): Promise { - return resolveIdpCall("userByName", [name], this.#namespace) as tailor.idp.User; + async userByName(name: string): Promise { + return resolveIdpCall("userByName", [name], this.#namespace) as IdpUser; } async createUser(input: { name: string; password?: string; disabled?: boolean; - }): Promise { - return resolveIdpCall("createUser", [input], this.#namespace) as tailor.idp.User; + }): Promise { + return resolveIdpCall("createUser", [input], this.#namespace) as IdpUser; } async updateUser(input: { id: string; @@ -705,8 +741,8 @@ class MockIdpClient { password?: string; clearPassword?: boolean; disabled?: boolean; - }): Promise { - return resolveIdpCall("updateUser", [input], this.#namespace) as tailor.idp.User; + }): Promise { + return resolveIdpCall("updateUser", [input], this.#namespace) as IdpUser; } async deleteUser(userId: string): Promise { return resolveIdpCall("deleteUser", [userId], this.#namespace) as boolean; diff --git a/packages/sdk/tsdown.config.ts b/packages/sdk/tsdown.config.ts index c38d8f760..93f8a0d18 100644 --- a/packages/sdk/tsdown.config.ts +++ b/packages/sdk/tsdown.config.ts @@ -32,6 +32,13 @@ export default defineConfig({ "src/vitest/setup.ts", "src/runtime/index.ts", "src/runtime/globals.ts", + "src/runtime/iconv.ts", + "src/runtime/secretmanager.ts", + "src/runtime/authconnection.ts", + "src/runtime/idp.ts", + "src/runtime/workflow.ts", + "src/runtime/context.ts", + "src/runtime/file.ts", ], format: ["esm"], target: "node22", From d5ad97ea555ea99c521d5875102fe18671190402 Mon Sep 17 00:00:00 2001 From: "tailor-platform-pr-trigger[bot]" <247949890+tailor-platform-pr-trigger[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 07:17:50 +0000 Subject: [PATCH 03/35] chore: sync skills --- skills/tailor-sdk/SKILL.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skills/tailor-sdk/SKILL.md b/skills/tailor-sdk/SKILL.md index ee1852d2a..7d1dcdcf2 100644 --- a/skills/tailor-sdk/SKILL.md +++ b/skills/tailor-sdk/SKILL.md @@ -17,6 +17,7 @@ Use these files as the single source of truth: - `node_modules/@tailor-platform/sdk/docs/cli-reference.md` - `node_modules/@tailor-platform/sdk/docs/cli/*.md` - `node_modules/@tailor-platform/sdk/docs/testing.md` +- `node_modules/@tailor-platform/sdk/docs/runtime.md` ## Working Rules @@ -32,3 +33,4 @@ Use these files as the single source of truth: - Service details: `docs/services/*.md` - CLI commands: `docs/cli-reference.md` and `docs/cli/*.md` - Testing patterns: `docs/testing.md` +- Runtime API wrappers (`tailor.iconv`, `tailor.secretmanager`, `tailor.idp`, `tailor.workflow`, `tailor.context`, `tailor.authconnection`, `tailordb.file`): `docs/runtime.md` From 0276343d2f0cba516107e541f20b07f8d9775cd5 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 13 May 2026 10:55:03 +0900 Subject: [PATCH 04/35] fix(sdk): use existing workflowMock API names in runtime workflow test Rename setWorkflowExecutionId -> setTriggerHandler and setWaitResult -> setWaitHandler in src/runtime/__tests__/workflow.test.ts. The previous identifiers do not exist on workflowMock and broke typecheck:go and unit tests on Node 22/24, Coverage, and SDK E2E. --- packages/sdk/src/runtime/__tests__/workflow.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/runtime/__tests__/workflow.test.ts b/packages/sdk/src/runtime/__tests__/workflow.test.ts index 1ac792c4b..82a70d0f4 100644 --- a/packages/sdk/src/runtime/__tests__/workflow.test.ts +++ b/packages/sdk/src/runtime/__tests__/workflow.test.ts @@ -17,7 +17,7 @@ describe("@tailor-platform/sdk/runtime/workflow", () => { }); test("triggerWorkflow forwards args and returns Promise", async () => { - workflowMock.setWorkflowExecutionId("exec-42"); + workflowMock.setTriggerHandler("exec-42"); const promise = workflow.triggerWorkflow("my-workflow", { a: 1 }); @@ -52,7 +52,7 @@ describe("@tailor-platform/sdk/runtime/workflow", () => { }); test("wait records the call and returns the configured result", () => { - workflowMock.setWaitResult({ resumed: true }); + workflowMock.setWaitHandler({ resumed: true }); const result = workflow.wait("key-1", { p: 1 }); From a84e8615d9538feae31b3f3f0a205a8125124d6e Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 13 May 2026 11:41:33 +0900 Subject: [PATCH 05/35] refactor(sdk): make runtime wrappers opt-in to ambient globals Split the runtime data types and the typed globalThis accessor into a private _runtime.ts module so that the wrappers under @tailor-platform/sdk/runtime/{iconv,secretmanager,authconnection,idp, workflow,context,file} no longer side-effect import ./globals. Previously, importing any wrapper transitively activated the declare global block in ./globals, which contradicted the documented opt-in contract (the user was supposed to explicitly import @tailor-platform/sdk/runtime/globals to get ambient tailor.* / tailordb types). After this change, only the explicit globals entry activates those declarations; the wrappers reach the runtime via a typed lazy runtime accessor that reads globalThis.tailor / globalThis.tailordb without polluting the consumer scope. Test files are updated to follow the same rule. --- .../runtime/__tests__/authconnection.test.ts | 1 - .../sdk/src/runtime/__tests__/context.test.ts | 1 - .../sdk/src/runtime/__tests__/file.test.ts | 1 - .../sdk/src/runtime/__tests__/globals.test.ts | 9 +- .../sdk/src/runtime/__tests__/iconv.test.ts | 1 - .../sdk/src/runtime/__tests__/idp.test.ts | 1 - .../runtime/__tests__/secretmanager.test.ts | 1 - .../src/runtime/__tests__/workflow.test.ts | 1 - packages/sdk/src/runtime/_runtime.ts | 423 ++++++++++++++++++ packages/sdk/src/runtime/authconnection.ts | 4 +- packages/sdk/src/runtime/context.ts | 5 +- packages/sdk/src/runtime/file.ts | 42 +- packages/sdk/src/runtime/globals.ts | 294 +----------- packages/sdk/src/runtime/iconv.ts | 20 +- packages/sdk/src/runtime/idp.ts | 27 +- packages/sdk/src/runtime/secretmanager.ts | 6 +- packages/sdk/src/runtime/workflow.ts | 11 +- packages/sdk/src/vitest/mock.ts | 3 +- 18 files changed, 500 insertions(+), 351 deletions(-) create mode 100644 packages/sdk/src/runtime/_runtime.ts diff --git a/packages/sdk/src/runtime/__tests__/authconnection.test.ts b/packages/sdk/src/runtime/__tests__/authconnection.test.ts index e2634c7de..59ec5395c 100644 --- a/packages/sdk/src/runtime/__tests__/authconnection.test.ts +++ b/packages/sdk/src/runtime/__tests__/authconnection.test.ts @@ -1,7 +1,6 @@ /** * Tests for `@tailor-platform/sdk/runtime/authconnection` typed wrappers. */ -import "@/runtime/globals"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import * as authconnection from "@/runtime/authconnection"; import { authconnectionMock, cleanupMocks, injectMocks } from "@/vitest/mock"; diff --git a/packages/sdk/src/runtime/__tests__/context.test.ts b/packages/sdk/src/runtime/__tests__/context.test.ts index 43e145ace..62b1dd878 100644 --- a/packages/sdk/src/runtime/__tests__/context.test.ts +++ b/packages/sdk/src/runtime/__tests__/context.test.ts @@ -1,7 +1,6 @@ /** * Tests for `@tailor-platform/sdk/runtime/context` typed wrappers. */ -import "@/runtime/globals"; import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; import * as context from "@/runtime/context"; import { cleanupMocks, injectMocks } from "@/vitest/mock"; diff --git a/packages/sdk/src/runtime/__tests__/file.test.ts b/packages/sdk/src/runtime/__tests__/file.test.ts index b76a31acb..f485a5ca9 100644 --- a/packages/sdk/src/runtime/__tests__/file.test.ts +++ b/packages/sdk/src/runtime/__tests__/file.test.ts @@ -1,7 +1,6 @@ /** * Tests for `@tailor-platform/sdk/runtime/file` typed wrappers. */ -import "@/runtime/globals"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import * as file from "@/runtime/file"; import { cleanupMocks, fileMock, injectMocks } from "@/vitest/mock"; diff --git a/packages/sdk/src/runtime/__tests__/globals.test.ts b/packages/sdk/src/runtime/__tests__/globals.test.ts index fc9496fbb..5acb283c8 100644 --- a/packages/sdk/src/runtime/__tests__/globals.test.ts +++ b/packages/sdk/src/runtime/__tests__/globals.test.ts @@ -1,16 +1,15 @@ /** - * Type-level tests confirming that importing the runtime entries activates - * the ambient `tailor.*` / `tailordb` globals declared in - * `src/runtime/globals.ts`. + * Type-level tests confirming that opting into `@tailor-platform/sdk/runtime/globals` + * activates the ambient `tailor.*` / `tailordb` declarations. * * These assertions are type-only — they reference `tailor`, `tailordb`, and * `TailorDBFileError` solely through `typeof` so the test does not require * the platform runtime to inject those values into the unit test environment. */ -import "@/runtime"; +import "@/runtime/globals"; import { describe, expectTypeOf, test } from "vitest"; -describe("@tailor-platform/sdk/runtime activates ambient globals", () => { +describe("@tailor-platform/sdk/runtime/globals activates ambient globals", () => { test("tailor.iconv.convert is declared as a function", () => { expectTypeOf().toBeFunction(); }); diff --git a/packages/sdk/src/runtime/__tests__/iconv.test.ts b/packages/sdk/src/runtime/__tests__/iconv.test.ts index 8b08d19c6..3488a5299 100644 --- a/packages/sdk/src/runtime/__tests__/iconv.test.ts +++ b/packages/sdk/src/runtime/__tests__/iconv.test.ts @@ -5,7 +5,6 @@ * via `iconvMock.calls`) and that the return-type narrowing (`UTF-8` → * `string`, otherwise `Uint8Array`) holds at the type level. */ -import "@/runtime/globals"; import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; import * as iconv from "@/runtime/iconv"; import { cleanupMocks, iconvMock, injectMocks } from "@/vitest/mock"; diff --git a/packages/sdk/src/runtime/__tests__/idp.test.ts b/packages/sdk/src/runtime/__tests__/idp.test.ts index 2d8e47f20..09ad71e3b 100644 --- a/packages/sdk/src/runtime/__tests__/idp.test.ts +++ b/packages/sdk/src/runtime/__tests__/idp.test.ts @@ -4,7 +4,6 @@ * Verifies that {@link idp.Client} forwards each method to the platform's * `tailor.idp.Client` and records calls with method, args, and namespace. */ -import "@/runtime/globals"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import * as idp from "@/runtime/idp"; import { cleanupMocks, idpMock, injectMocks } from "@/vitest/mock"; diff --git a/packages/sdk/src/runtime/__tests__/secretmanager.test.ts b/packages/sdk/src/runtime/__tests__/secretmanager.test.ts index 69a61b6e6..4c787ae11 100644 --- a/packages/sdk/src/runtime/__tests__/secretmanager.test.ts +++ b/packages/sdk/src/runtime/__tests__/secretmanager.test.ts @@ -1,7 +1,6 @@ /** * Tests for `@tailor-platform/sdk/runtime/secretmanager` typed wrappers. */ -import "@/runtime/globals"; import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; import * as secretmanager from "@/runtime/secretmanager"; import { cleanupMocks, injectMocks, secretmanagerMock } from "@/vitest/mock"; diff --git a/packages/sdk/src/runtime/__tests__/workflow.test.ts b/packages/sdk/src/runtime/__tests__/workflow.test.ts index 82a70d0f4..a87f6a497 100644 --- a/packages/sdk/src/runtime/__tests__/workflow.test.ts +++ b/packages/sdk/src/runtime/__tests__/workflow.test.ts @@ -1,7 +1,6 @@ /** * Tests for `@tailor-platform/sdk/runtime/workflow` typed wrappers. */ -import "@/runtime/globals"; import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; import * as workflow from "@/runtime/workflow"; import { cleanupMocks, injectMocks, workflowMock } from "@/vitest/mock"; diff --git a/packages/sdk/src/runtime/_runtime.ts b/packages/sdk/src/runtime/_runtime.ts new file mode 100644 index 000000000..1cfeb9e8b --- /dev/null +++ b/packages/sdk/src/runtime/_runtime.ts @@ -0,0 +1,423 @@ +/** + * Internal runtime bindings shared by the typed wrappers in + * `@tailor-platform/sdk/runtime/*`. Not part of the public API. + * + * - The exported `runtime` value reads `tailor` / `tailordb` from `globalThis` + * lazily through getters so wrappers stay decoupled from module-load order + * (mocks injected in `beforeEach` are picked up on next access). + * - The exported module-scope types describe the platform runtime surface + * without introducing any ambient global declarations. The `declare global` + * block lives only in `./globals`, which callers opt into explicitly. + * @internal + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +// --------------------------------------------------------------------------- +// Module-scope data types +// --------------------------------------------------------------------------- + +// --- Tailordb ------------------------------------------------------------- + +/** Result of a single `queryObject` call against the TailorDB driver. */ +export interface TailordbQueryResult { + rows: T[]; + command: TailordbCommandType; + rowCount: number; +} + +/** SQL command type recorded on a {@link TailordbQueryResult}. */ +export type TailordbCommandType = + | "INSERT" + | "DELETE" + | "UPDATE" + | "SELECT" + | "MOVE" + | "FETCH" + | "COPY" + | "CREATE"; + +// --- TailorDB file API --------------------------------------------------- + +/** Upload response metadata. */ +export interface UploadMetadata { + fileSize: number; + sha256sum: string; +} + +/** Download response metadata. */ +export interface DownloadMetadata { + contentType: string; + fileSize: number; + sha256sum: string; + lastUploadedAt: string; +} + +/** File metadata (for `getMetadata`). */ +export interface FileMetadata { + contentType: string; + fileSize: number; + sha256sum: string; + urlPath: string; + lastUploadedAt?: string; +} + +/** Stream metadata (first chunk). */ +export interface StreamMetadata { + contentType: string; + fileSize: number; + sha256sum: string; +} + +/** Upload options. */ +export interface FileUploadOptions { + contentType?: string; +} + +/** Upload response. */ +export interface FileUploadResponse { + metadata: UploadMetadata; +} + +/** Download response. */ +export interface FileDownloadResponse { + data: Uint8Array; + metadata: DownloadMetadata; +} + +/** Download-as-Base64 response. */ +export interface FileDownloadAsBase64Response { + data: string; + metadata: DownloadMetadata; +} + +/** Stream chunk types. */ +export type StreamValue = + | { type: "metadata"; metadata: StreamMetadata } + | { type: "chunk"; data: Uint8Array; position: number } + | { type: "complete" }; + +/** Stream iterator interface. */ +export interface FileStreamIterator extends AsyncIterableIterator { + next(): Promise>; + close(): Promise; +} + +/** TailorDB File API surface. */ +export interface TailorDBFileAPI { + upload( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + data: string | ArrayBuffer | Uint8Array | number[], + options?: FileUploadOptions, + ): Promise; + + download( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + ): Promise; + + downloadAsBase64( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + ): Promise; + + delete(namespace: string, typeName: string, fieldName: string, recordId: string): Promise; + + getMetadata( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + ): Promise; + + openDownloadStream( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + ): Promise; +} + +/** Error code emitted by `TailorDBFileError`. */ +export type TailorDBFileErrorCode = + | "INVALID_PARAMS" + | "INVALID_DATA_TYPE" + | "OPERATION_FAILED" + | "DELETE_FAILED" + | "STREAM_OPEN_FAILED" + | "STREAM_READ_ERROR" + | "STREAM_ERROR" + | "FILE_TOO_LARGE"; + +/** + * Type-only shape of the `TailorDBFileError` runtime class. The class itself + * is provided by the platform runtime (and by `injectMocks` in tests); this + * interface mirrors it so callers can `import type { TailorDBFileError }` from + * the wrapper module without depending on any ambient declaration. + */ +export interface TailorDBFileError extends Error { + name: "TailorDBFileError"; + code?: TailorDBFileErrorCode; + cause?: unknown; +} + +// --- tailor.idp ----------------------------------------------------------- + +/** Configuration for creating an IDP Client. */ +export interface IdpClientConfig { + namespace: string; +} + +/** User object returned from IDP operations. */ +export interface IdpUser { + id: string; + name: string; + disabled: boolean; + createdAt?: string; + updatedAt?: string; +} + +/** Query options for filtering users. */ +export interface IdpUserQuery { + /** Filter by user IDs */ + ids?: string[]; + /** Filter by user names */ + names?: string[]; +} + +/** Options for listing users. */ +export interface IdpListUsersOptions { + /** Maximum number of users to return */ + first?: number; + /** Page token for pagination */ + after?: string; + /** Query filter for users */ + query?: IdpUserQuery; +} + +/** Response from listing users. */ +export interface IdpListUsersResponse { + users: IdpUser[]; + nextPageToken: string | null; + totalCount: number; +} + +/** Input for creating a new user. */ +export interface IdpCreateUserInput { + /** The user's name (typically email) */ + name: string; + /** The user's password. If omitted, the user is created without a password (cannot log in with any password). */ + password?: string; + /** Whether the user is disabled */ + disabled?: boolean; +} + +/** Input for updating an existing user. */ +export interface IdpUpdateUserInput { + /** The user's ID */ + id: string; + /** New name for the user */ + name?: string; + /** New password for the user. Cannot be used with clearPassword. */ + password?: string; + /** If true, remove the user's password. Cannot be used with password. */ + clearPassword?: boolean; + /** New disabled status for the user */ + disabled?: boolean; +} + +/** Input for sending a password reset email. */ +export interface IdpSendPasswordResetEmailInput { + /** The ID of the user */ + userId: string; + /** The URI to redirect to after password reset */ + redirectUri: string; + /** The sender display name. Defaults to 'Tailor Platform IdP'. */ + fromName?: string; + /** The email subject line. Defaults to the localized default subject. */ + subject?: string; +} + +/** Instance methods exposed by `tailor.idp.Client`. */ +export interface IdpClientInstance { + users(options?: IdpListUsersOptions): Promise; + user(userId: string): Promise; + userByName(name: string): Promise; + createUser(input: IdpCreateUserInput): Promise; + updateUser(input: IdpUpdateUserInput): Promise; + deleteUser(userId: string): Promise; + sendPasswordResetEmail(input: IdpSendPasswordResetEmailInput): Promise; +} + +/** Constructor shape for `tailor.idp.Client`. */ +export interface IdpClientConstructor { + new (config: IdpClientConfig): IdpClientInstance; +} + +// --- tailor.workflow ----------------------------------------------------- + +/** + * Specifies the machine user that should be used to execute the workflow. + * This allows workflows to run with specific authentication context. + */ +export interface WorkflowAuthInvoker { + /** The namespace where the machine user is defined */ + namespace: string; + /** The name of the machine user to use for workflow execution */ + machineUserName: string; +} + +/** Options for triggering a workflow. */ +export interface WorkflowTriggerWorkflowOptions { + /** Optional authentication invoker to specify which machine user should execute the workflow */ + authInvoker?: WorkflowAuthInvoker; +} + +// --- tailor.context ------------------------------------------------------- + +/** Information about the invoker of the current function execution. */ +export interface ContextInvoker { + /** The invoker's ID */ + id: string; + /** The invoker's type */ + type: "user" | "machine_user"; + /** The workspace ID */ + workspaceId: string; + /** The invoker's attribute IDs */ + attributes: string[]; + /** The invoker's attribute map */ + attributeMap: Record; +} + +// --- tailor.iconv --------------------------------------------------------- + +/** Instance methods exposed by `tailor.iconv.Iconv`. */ +export interface IconvInstance { + convert(input: string | Uint8Array | ArrayBuffer): string | Uint8Array; +} + +/** Constructor shape for `tailor.iconv.Iconv`. */ +export interface IconvConstructor { + new (fromEncoding: string, toEncoding: string): IconvInstance; +} + +// --------------------------------------------------------------------------- +// API surface types — describe the shape of `globalThis.tailor` / `tailordb` +// without polluting any ambient namespace +// --------------------------------------------------------------------------- + +/** `tailor.secretmanager` API surface. */ +export interface TailorSecretmanagerAPI { + getSecrets( + vault: string, + names: T, + ): Promise>>; + getSecret(vault: string, name: string): Promise; +} + +/** `tailor.authconnection` API surface. */ +export interface TailorAuthconnectionAPI { + getConnectionToken(connectionName: string): Promise; +} + +/** `tailor.iconv` API surface. */ +export interface TailorIconvAPI { + convert( + str: string | Uint8Array | ArrayBuffer, + fromEncoding: string, + toEncoding: T, + ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; + convertBuffer( + buffer: Uint8Array | ArrayBuffer, + fromEncoding: string, + toEncoding: T, + ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; + decode(buffer: Uint8Array | ArrayBuffer, encoding: string): string; + encode( + str: string, + encoding: T, + ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; + encodings(): string[]; + Iconv: IconvConstructor; +} + +/** `tailor.idp` API surface. */ +export interface TailorIdpAPI { + Client: IdpClientConstructor; +} + +/** `tailor.workflow` API surface. */ +export interface TailorWorkflowAPI { + triggerWorkflow( + workflow_name: string, + args?: any, + options?: WorkflowTriggerWorkflowOptions, + ): Promise; + triggerJobFunction(job_name: string, args?: any): any; + wait(key: string, payload?: any): any; + resolve(executionId: string, key: string, callback: (waitPayload: any) => any): Promise; +} + +/** `tailor.context` API surface. */ +export interface TailorContextAPI { + getInvoker(): ContextInvoker | null; +} + +/** Top-level `tailor` runtime object. */ +export interface TailorRuntime { + secretmanager: TailorSecretmanagerAPI; + authconnection: TailorAuthconnectionAPI; + iconv: TailorIconvAPI; + idp: TailorIdpAPI; + workflow: TailorWorkflowAPI; + context: TailorContextAPI; +} + +/** Instance methods exposed by `tailordb.Client`. */ +export interface TailordbClientInstance { + connect(): Promise; + end(): Promise; + queryObject(sql: string, args?: readonly unknown[]): Promise>; +} + +/** Constructor shape for `tailordb.Client`. */ +export interface TailordbClientConstructor { + new (config: { namespace: string }): TailordbClientInstance; +} + +/** Top-level `tailordb` runtime object. */ +export interface TailordbRuntime { + Client: TailordbClientConstructor; + file: TailorDBFileAPI; +} + +// --------------------------------------------------------------------------- +// Typed accessor — reads `tailor` / `tailordb` from globalThis lazily. +// Importing this value does NOT activate any ambient global declarations. +// --------------------------------------------------------------------------- + +interface RuntimeBindings { + readonly tailor: TailorRuntime; + readonly tailordb: TailordbRuntime; +} + +/** + * Lazy typed view of the platform runtime globals (`tailor`, `tailordb`). + * Each property read returns the current `globalThis` value, so test setups + * that inject mocks in `beforeEach` work without needing to re-import. + */ +export const runtime: RuntimeBindings = { + get tailor() { + return (globalThis as unknown as { tailor: TailorRuntime }).tailor; + }, + get tailordb() { + return (globalThis as unknown as { tailordb: TailordbRuntime }).tailordb; + }, +}; diff --git a/packages/sdk/src/runtime/authconnection.ts b/packages/sdk/src/runtime/authconnection.ts index 3736644cf..41c5430ac 100644 --- a/packages/sdk/src/runtime/authconnection.ts +++ b/packages/sdk/src/runtime/authconnection.ts @@ -10,7 +10,7 @@ * const token = await authconnection.getConnectionToken("my-connection"); */ -import "./globals"; +import { runtime } from "./_runtime"; /** * Returns the access token for the given auth connection. @@ -19,5 +19,5 @@ import "./globals"; */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function getConnectionToken(connectionName: string): Promise { - return tailor.authconnection.getConnectionToken(connectionName); + return runtime.tailor.authconnection.getConnectionToken(connectionName); } diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index c4f9cf982..00dcc5b77 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -13,8 +13,7 @@ * } */ -import "./globals"; -import type { ContextInvoker } from "./globals"; +import { runtime, type ContextInvoker } from "./_runtime"; /** Information about the invoker of the current function execution. */ export type Invoker = ContextInvoker; @@ -25,5 +24,5 @@ export type Invoker = ContextInvoker; * @returns Invoker details, or `null` when the call is anonymous */ export function getInvoker(): Invoker | null { - return tailor.context.getInvoker(); + return runtime.tailor.context.getInvoker(); } diff --git a/packages/sdk/src/runtime/file.ts b/packages/sdk/src/runtime/file.ts index 3ba7e8ce8..93a4f10ab 100644 --- a/packages/sdk/src/runtime/file.ts +++ b/packages/sdk/src/runtime/file.ts @@ -16,21 +16,21 @@ * ); */ -import "./globals"; -import type { - UploadMetadata, - DownloadMetadata, - FileMetadata, - StreamMetadata, - FileUploadOptions, - FileUploadResponse, - FileDownloadResponse, - FileDownloadAsBase64Response, - StreamValue, - FileStreamIterator, - TailorDBFileError, - TailorDBFileErrorCode, -} from "./globals"; +import { + runtime, + type DownloadMetadata, + type FileDownloadAsBase64Response, + type FileDownloadResponse, + type FileMetadata, + type FileStreamIterator, + type FileUploadOptions, + type FileUploadResponse, + type StreamMetadata, + type StreamValue, + type TailorDBFileError, + type TailorDBFileErrorCode, + type UploadMetadata, +} from "./_runtime"; export type { UploadMetadata, @@ -65,7 +65,7 @@ export function upload( data: string | ArrayBuffer | Uint8Array | number[], options?: FileUploadOptions, ): Promise { - return tailordb.file.upload(namespace, typeName, fieldName, recordId, data, options); + return runtime.tailordb.file.upload(namespace, typeName, fieldName, recordId, data, options); } /** @@ -85,7 +85,7 @@ export function download( fieldName: string, recordId: string, ): Promise { - return tailordb.file.download(namespace, typeName, fieldName, recordId); + return runtime.tailordb.file.download(namespace, typeName, fieldName, recordId); } /** @@ -105,7 +105,7 @@ export function downloadAsBase64( fieldName: string, recordId: string, ): Promise { - return tailordb.file.downloadAsBase64(namespace, typeName, fieldName, recordId); + return runtime.tailordb.file.downloadAsBase64(namespace, typeName, fieldName, recordId); } /** @@ -122,7 +122,7 @@ function deleteFile( fieldName: string, recordId: string, ): Promise { - return tailordb.file.delete(namespace, typeName, fieldName, recordId); + return runtime.tailordb.file.delete(namespace, typeName, fieldName, recordId); } /** @@ -139,7 +139,7 @@ export function getMetadata( fieldName: string, recordId: string, ): Promise { - return tailordb.file.getMetadata(namespace, typeName, fieldName, recordId); + return runtime.tailordb.file.getMetadata(namespace, typeName, fieldName, recordId); } /** @@ -156,7 +156,7 @@ export function openDownloadStream( fieldName: string, recordId: string, ): Promise { - return tailordb.file.openDownloadStream(namespace, typeName, fieldName, recordId); + return runtime.tailordb.file.openDownloadStream(namespace, typeName, fieldName, recordId); } export { deleteFile as delete }; diff --git a/packages/sdk/src/runtime/globals.ts b/packages/sdk/src/runtime/globals.ts index 5f5a6e11e..49cf260bf 100644 --- a/packages/sdk/src/runtime/globals.ts +++ b/packages/sdk/src/runtime/globals.ts @@ -18,283 +18,23 @@ /* eslint-disable @typescript-eslint/no-namespace, @typescript-eslint/no-explicit-any, jsdoc/require-param, jsdoc/require-returns, jsdoc/require-param-description */ -// --------------------------------------------------------------------------- -// Module-scope types (exported, non-global) -// -// These types describe the data shapes used by the platform runtime. The -// `declare global` block below aliases each of them into the appropriate -// `tailor.*` / global namespace, so callers who opt into the globals see the -// same surface they always did. Callers who do not opt in can still import -// these types directly via `@tailor-platform/sdk/runtime/*` — none of the -// types below reference globals, so they are self-contained. -// --------------------------------------------------------------------------- - -// --- Tailordb ------------------------------------------------------------- - -/** Result of a single `queryObject` call against the TailorDB driver. */ -export interface TailordbQueryResult { - rows: T[]; - command: TailordbCommandType; - rowCount: number; -} - -/** SQL command type recorded on a {@link TailordbQueryResult}. */ -export type TailordbCommandType = - | "INSERT" - | "DELETE" - | "UPDATE" - | "SELECT" - | "MOVE" - | "FETCH" - | "COPY" - | "CREATE"; - -// --- TailorDB file API --------------------------------------------------- - -/** Upload response metadata. */ -export interface UploadMetadata { - fileSize: number; - sha256sum: string; -} - -/** Download response metadata. */ -export interface DownloadMetadata { - contentType: string; - fileSize: number; - sha256sum: string; - lastUploadedAt: string; -} - -/** File metadata (for `getMetadata`). */ -export interface FileMetadata { - contentType: string; - fileSize: number; - sha256sum: string; - urlPath: string; - lastUploadedAt?: string; -} - -/** Stream metadata (first chunk). */ -export interface StreamMetadata { - contentType: string; - fileSize: number; - sha256sum: string; -} - -/** Upload options. */ -export interface FileUploadOptions { - contentType?: string; -} - -/** Upload response. */ -export interface FileUploadResponse { - metadata: UploadMetadata; -} - -/** Download response. */ -export interface FileDownloadResponse { - data: Uint8Array; - metadata: DownloadMetadata; -} - -/** Download-as-Base64 response. */ -export interface FileDownloadAsBase64Response { - data: string; - metadata: DownloadMetadata; -} - -/** Stream chunk types. */ -export type StreamValue = - | { type: "metadata"; metadata: StreamMetadata } - | { type: "chunk"; data: Uint8Array; position: number } - | { type: "complete" }; - -/** Stream iterator interface. */ -export interface FileStreamIterator extends AsyncIterableIterator { - next(): Promise>; - close(): Promise; -} - -/** TailorDB File API surface. */ -export interface TailorDBFileAPI { - upload( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - data: string | ArrayBuffer | Uint8Array | number[], - options?: FileUploadOptions, - ): Promise; - - download( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - ): Promise; - - downloadAsBase64( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - ): Promise; - - delete(namespace: string, typeName: string, fieldName: string, recordId: string): Promise; - - getMetadata( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - ): Promise; - - openDownloadStream( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - ): Promise; -} - -/** Error code emitted by `TailorDBFileError`. */ -export type TailorDBFileErrorCode = - | "INVALID_PARAMS" - | "INVALID_DATA_TYPE" - | "OPERATION_FAILED" - | "DELETE_FAILED" - | "STREAM_OPEN_FAILED" - | "STREAM_READ_ERROR" - | "STREAM_ERROR" - | "FILE_TOO_LARGE"; - -/** - * Type-only shape of the `TailorDBFileError` runtime class. The class itself - * is declared globally below; this interface mirrors it so callers can use - * `import type { TailorDBFileError } from "@tailor-platform/sdk/runtime/file"` - * without depending on the global declaration. - */ -export interface TailorDBFileError extends Error { - name: "TailorDBFileError"; - code?: TailorDBFileErrorCode; - cause?: unknown; -} - -// --- tailor.idp ----------------------------------------------------------- - -/** Configuration for creating an IDP Client. */ -export interface IdpClientConfig { - namespace: string; -} - -/** User object returned from IDP operations. */ -export interface IdpUser { - id: string; - name: string; - disabled: boolean; - createdAt?: string; - updatedAt?: string; -} - -/** Query options for filtering users. */ -export interface IdpUserQuery { - /** Filter by user IDs */ - ids?: string[]; - /** Filter by user names */ - names?: string[]; -} - -/** Options for listing users. */ -export interface IdpListUsersOptions { - /** Maximum number of users to return */ - first?: number; - /** Page token for pagination */ - after?: string; - /** Query filter for users */ - query?: IdpUserQuery; -} - -/** Response from listing users. */ -export interface IdpListUsersResponse { - users: IdpUser[]; - nextPageToken: string | null; - totalCount: number; -} - -/** Input for creating a new user. */ -export interface IdpCreateUserInput { - /** The user's name (typically email) */ - name: string; - /** The user's password. If omitted, the user is created without a password (cannot log in with any password). */ - password?: string; - /** Whether the user is disabled */ - disabled?: boolean; -} - -/** Input for updating an existing user. */ -export interface IdpUpdateUserInput { - /** The user's ID */ - id: string; - /** New name for the user */ - name?: string; - /** New password for the user. Cannot be used with clearPassword. */ - password?: string; - /** If true, remove the user's password. Cannot be used with password. */ - clearPassword?: boolean; - /** New disabled status for the user */ - disabled?: boolean; -} - -/** Input for sending a password reset email. */ -export interface IdpSendPasswordResetEmailInput { - /** The ID of the user */ - userId: string; - /** The URI to redirect to after password reset */ - redirectUri: string; - /** The sender display name. Defaults to 'Tailor Platform IdP'. */ - fromName?: string; - /** The email subject line. Defaults to the localized default subject. */ - subject?: string; -} - -// --- tailor.workflow ----------------------------------------------------- - -/** - * Specifies the machine user that should be used to execute the workflow. - * This allows workflows to run with specific authentication context. - */ -export interface WorkflowAuthInvoker { - /** The namespace where the machine user is defined */ - namespace: string; - /** The name of the machine user to use for workflow execution */ - machineUserName: string; -} - -/** Options for triggering a workflow. */ -export interface WorkflowTriggerWorkflowOptions { - /** Optional authentication invoker to specify which machine user should execute the workflow */ - authInvoker?: WorkflowAuthInvoker; -} - -// --- tailor.context ------------------------------------------------------- - -/** Information about the invoker of the current function execution. */ -export interface ContextInvoker { - /** The invoker's ID */ - id: string; - /** The invoker's type */ - type: "user" | "machine_user"; - /** The workspace ID */ - workspaceId: string; - /** The invoker's attribute IDs */ - attributes: string[]; - /** The invoker's attribute map */ - attributeMap: Record; -} - -// --------------------------------------------------------------------------- -// Ambient globals — alias the module-scope types into the runtime namespaces -// --------------------------------------------------------------------------- +import type { + ContextInvoker, + IdpClientConfig, + IdpCreateUserInput, + IdpListUsersOptions, + IdpListUsersResponse, + IdpSendPasswordResetEmailInput, + IdpUpdateUserInput, + IdpUser, + IdpUserQuery, + TailorDBFileAPI, + TailorDBFileErrorCode, + TailordbCommandType, + TailordbQueryResult, + WorkflowAuthInvoker, + WorkflowTriggerWorkflowOptions, +} from "./_runtime"; declare global { namespace Tailordb { diff --git a/packages/sdk/src/runtime/iconv.ts b/packages/sdk/src/runtime/iconv.ts index a7c95d408..e876099ea 100644 --- a/packages/sdk/src/runtime/iconv.ts +++ b/packages/sdk/src/runtime/iconv.ts @@ -15,7 +15,7 @@ * const out = conv.convert(sjisBuffer); */ -import "./globals"; +import { runtime, type IconvInstance } from "./_runtime"; /** * Convert a string or buffer between encodings. @@ -29,7 +29,7 @@ export function convert( fromEncoding: string, toEncoding: T, ): T extends "UTF8" | "UTF-8" ? string : Uint8Array { - return tailor.iconv.convert(str, fromEncoding, toEncoding); + return runtime.tailor.iconv.convert(str, fromEncoding, toEncoding); } /** @@ -44,7 +44,7 @@ export function convertBuffer( fromEncoding: string, toEncoding: T, ): T extends "UTF8" | "UTF-8" ? string : Uint8Array { - return tailor.iconv.convertBuffer(buffer, fromEncoding, toEncoding); + return runtime.tailor.iconv.convertBuffer(buffer, fromEncoding, toEncoding); } /** @@ -54,7 +54,7 @@ export function convertBuffer( * @returns Decoded UTF-8 string */ export function decode(buffer: Uint8Array | ArrayBuffer, encoding: string): string { - return tailor.iconv.decode(buffer, encoding); + return runtime.tailor.iconv.decode(buffer, encoding); } /** @@ -67,7 +67,7 @@ export function encode( str: string, encoding: T, ): T extends "UTF8" | "UTF-8" ? string : Uint8Array { - return tailor.iconv.encode(str, encoding); + return runtime.tailor.iconv.encode(str, encoding); } /** @@ -75,11 +75,7 @@ export function encode( * @returns Array of encoding names supported by the platform iconv runtime */ export function encodings(): string[] { - return tailor.iconv.encodings(); -} - -interface IconvImpl { - convert(input: string | Uint8Array | ArrayBuffer): string | Uint8Array; + return runtime.tailor.iconv.encodings(); } /** @@ -87,10 +83,10 @@ interface IconvImpl { * Compatible with the `node-iconv` API surface. */ export class Iconv { - private impl: IconvImpl; + private impl: IconvInstance; constructor(fromEncoding: string, toEncoding: string) { - this.impl = new tailor.iconv.Iconv(fromEncoding, toEncoding); + this.impl = new runtime.tailor.iconv.Iconv(fromEncoding, toEncoding); } /** diff --git a/packages/sdk/src/runtime/idp.ts b/packages/sdk/src/runtime/idp.ts index d994e04f1..e76cd1494 100644 --- a/packages/sdk/src/runtime/idp.ts +++ b/packages/sdk/src/runtime/idp.ts @@ -11,17 +11,18 @@ * const { users } = await client.users({ first: 10 }); */ -import "./globals"; -import type { - IdpClientConfig, - IdpUser, - IdpUserQuery, - IdpListUsersOptions, - IdpListUsersResponse, - IdpCreateUserInput, - IdpUpdateUserInput, - IdpSendPasswordResetEmailInput, -} from "./globals"; +import { + runtime, + type IdpClientConfig, + type IdpClientInstance, + type IdpCreateUserInput, + type IdpListUsersOptions, + type IdpListUsersResponse, + type IdpSendPasswordResetEmailInput, + type IdpUpdateUserInput, + type IdpUser, + type IdpUserQuery, +} from "./_runtime"; /** Configuration object for {@link Client}. */ export type ClientConfig = IdpClientConfig; @@ -53,10 +54,10 @@ export type SendPasswordResetEmailInput = IdpSendPasswordResetEmailInput; * Wraps the platform-provided `tailor.idp.Client` and exposes the same surface. */ export class Client { - #impl: tailor.idp.Client; + #impl: IdpClientInstance; constructor(config: ClientConfig) { - this.#impl = new tailor.idp.Client(config); + this.#impl = new runtime.tailor.idp.Client(config); } /** diff --git a/packages/sdk/src/runtime/secretmanager.ts b/packages/sdk/src/runtime/secretmanager.ts index 1cf0ca109..19faba3f5 100644 --- a/packages/sdk/src/runtime/secretmanager.ts +++ b/packages/sdk/src/runtime/secretmanager.ts @@ -12,7 +12,7 @@ * const all = await secretmanager.getSecrets("my-vault", ["A", "B"] as const); */ -import "./globals"; +import { runtime } from "./_runtime"; /** * Returns multiple secrets from a vault. Missing names are omitted from the result. @@ -24,7 +24,7 @@ export function getSecrets( vault: string, names: T, ): Promise>> { - return tailor.secretmanager.getSecrets(vault, names); + return runtime.tailor.secretmanager.getSecrets(vault, names); } /** @@ -34,5 +34,5 @@ export function getSecrets( * @returns The secret value, or `undefined` if not present */ export function getSecret(vault: string, name: string): Promise { - return tailor.secretmanager.getSecret(vault, name); + return runtime.tailor.secretmanager.getSecret(vault, name); } diff --git a/packages/sdk/src/runtime/workflow.ts b/packages/sdk/src/runtime/workflow.ts index 38750c31b..fa40c8db9 100644 --- a/packages/sdk/src/runtime/workflow.ts +++ b/packages/sdk/src/runtime/workflow.ts @@ -10,8 +10,7 @@ * const executionId = await workflow.triggerWorkflow("myWorkflow", { data: "value" }); */ -import "./globals"; -import type { WorkflowAuthInvoker, WorkflowTriggerWorkflowOptions } from "./globals"; +import { runtime, type WorkflowAuthInvoker, type WorkflowTriggerWorkflowOptions } from "./_runtime"; /** {@link triggerWorkflow} option type. */ export type AuthInvoker = WorkflowAuthInvoker; @@ -32,7 +31,7 @@ export function triggerWorkflow( args?: any, options?: TriggerWorkflowOptions, ): Promise { - return tailor.workflow.triggerWorkflow(workflow_name, args, options); + return runtime.tailor.workflow.triggerWorkflow(workflow_name, args, options); } /** @@ -46,7 +45,7 @@ export function triggerWorkflow( */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function triggerJobFunction(job_name: string, args?: any): any { - return tailor.workflow.triggerJobFunction(job_name, args); + return runtime.tailor.workflow.triggerJobFunction(job_name, args); } /** @@ -57,7 +56,7 @@ export function triggerJobFunction(job_name: string, args?: any): any { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function wait(key: string, payload?: any): any { - return tailor.workflow.wait(key, payload); + return runtime.tailor.workflow.wait(key, payload); } /** @@ -73,5 +72,5 @@ export function resolve( // eslint-disable-next-line @typescript-eslint/no-explicit-any callback: (waitPayload: any) => any, ): Promise { - return tailor.workflow.resolve(executionId, key, callback); + return runtime.tailor.workflow.resolve(executionId, key, callback); } diff --git a/packages/sdk/src/vitest/mock.ts b/packages/sdk/src/vitest/mock.ts index 43d4159a7..d8baf93b0 100644 --- a/packages/sdk/src/vitest/mock.ts +++ b/packages/sdk/src/vitest/mock.ts @@ -6,8 +6,7 @@ * responses and assert on recorded calls via the exported mock objects. */ -import "../runtime/globals"; -import type { ContextInvoker, IdpUser } from "../runtime/globals"; +import type { ContextInvoker, IdpUser } from "../runtime/_runtime"; // --------------------------------------------------------------------------- // Types From 1ae8a02c9c915c215bbda906f7bd4c9414205361 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 13 May 2026 14:44:35 +0900 Subject: [PATCH 06/35] docs(sdk): correct contextMock references and document it in testing guide --- packages/sdk/docs/testing.md | 29 ++++++++++++++++++++++++++++- packages/sdk/src/runtime/context.ts | 4 ++-- packages/sdk/src/vitest/index.ts | 4 ++-- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index 7597673d8..f02ca8265 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -24,7 +24,7 @@ Platform API mocks under `@tailor-platform/sdk/vitest` (auto-injected by the [`t - `tailordbMock` — TailorDB query stubs and call recording - `workflowMock` — `tailor.workflow` job / wait / resolve mocks -- `secretmanagerMock`, `authconnectionMock`, `idpMock`, `fileMock`, `iconvMock` — corresponding platform API mocks +- `secretmanagerMock`, `authconnectionMock`, `idpMock`, `fileMock`, `iconvMock`, `contextMock` — corresponding platform API mocks For tighter alignment with the production runtime — Node.js module blocking, Web-only globals, and platform API mocks — pair the resolver helpers with the [`tailor-runtime` Vitest environment](#runtime-environment-emulation-beta) below. @@ -247,6 +247,33 @@ test("mock encoding conversion", () => { }); ``` +### Context Mock + +```typescript +import { contextMock } from "@tailor-platform/sdk/vitest"; + +beforeEach(() => contextMock.reset()); + +test("returns invoker information", () => { + contextMock.setInvoker({ + id: "f1e2d3c4-b5a6-4798-89a0-1b2c3d4e5f60", + type: "machine_user", + workspaceId: "b39bdd61-d442-4a4e-8599-33a78a4e19ab", + }); + + const invoker = tailor.context.getInvoker(); + expect(invoker?.type).toBe("machine_user"); + expect(contextMock.calls).toHaveLength(1); +}); + +test("anonymous caller", () => { + contextMock.setInvoker(null); // null is the default + + const invoker = tailor.context.getInvoker(); + expect(invoker).toBeNull(); +}); +``` + ### Loading Secrets from Config Pass a config path to load `defineSecretManager()` values into the mock: diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index 00dcc5b77..7501c60c3 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -2,8 +2,8 @@ * Execution context utilities. * * Thin typed wrapper around the platform-provided `tailor.context` runtime API. - * At runtime this delegates to `globalThis.tailor.context`. Use - * `setupInvokerMock` from `@tailor-platform/sdk/test` to mock in unit tests. + * At runtime this delegates to `globalThis.tailor.context`. Use `contextMock` + * from `@tailor-platform/sdk/vitest` to mock these calls in unit tests. * @example * import { context } from "@tailor-platform/sdk/runtime"; * diff --git a/packages/sdk/src/vitest/index.ts b/packages/sdk/src/vitest/index.ts index e88318f76..d86a75d77 100644 --- a/packages/sdk/src/vitest/index.ts +++ b/packages/sdk/src/vitest/index.ts @@ -22,8 +22,8 @@ import type { Plugin } from "vitest/config"; * * 3. **Platform API mocks** (environment) — All platform APIs are auto-injected with * control objects: `tailordbMock`, `workflowMock`, `secretmanagerMock`, - * `authconnectionMock`, `idpMock`, `fileMock`, `iconvMock`. Each provides response - * configuration, call recording, and reset. + * `authconnectionMock`, `idpMock`, `fileMock`, `iconvMock`, `contextMock`. Each + * provides response configuration, call recording, and reset. * * 4. **Environment resolution** — Rewrites `environment: "tailor-runtime"` to the * absolute path of the bundled environment module via the config hook. From 4538886b93989a239bdc8b38570f1b53fad02041 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 13 May 2026 15:04:33 +0900 Subject: [PATCH 07/35] refactor(sdk): unify runtime/context.Invoker with SDK-friendly attribute shape --- example/tests/bundled_execution.test.ts | 4 +- packages/sdk/docs/testing.md | 13 ++++-- .../sdk/src/runtime/__tests__/context.test.ts | 25 ++++++++++- packages/sdk/src/runtime/context.ts | 43 ++++++++++++++++--- packages/sdk/src/vitest/mock.ts | 21 +++++++-- 5 files changed, 90 insertions(+), 16 deletions(-) diff --git a/example/tests/bundled_execution.test.ts b/example/tests/bundled_execution.test.ts index cf7c69abf..6f13a4daf 100644 --- a/example/tests/bundled_execution.test.ts +++ b/example/tests/bundled_execution.test.ts @@ -92,8 +92,8 @@ describe("bundled execution tests", () => { id: "f1e2d3c4-b5a6-4798-89a0-1b2c3d4e5f60", type: "machine_user", workspaceId: "b39bdd61-d442-4a4e-8599-33a78a4e19ab", - attributes: [], - attributeMap: { role: "MANAGER" }, + attributes: { role: "MANAGER" }, + attributeList: [], }); const main = await importActualMain("resolvers/showUserInfo.js"); diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index f02ca8265..ddd725b11 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -26,6 +26,8 @@ Platform API mocks under `@tailor-platform/sdk/vitest` (auto-injected by the [`t - `workflowMock` — `tailor.workflow` job / wait / resolve mocks - `secretmanagerMock`, `authconnectionMock`, `idpMock`, `fileMock`, `iconvMock`, `contextMock` — corresponding platform API mocks +> The examples below call `tailor.*` / `tailordb.*` via the ambient globals. To make these snippets type-check in a fresh TypeScript project, either opt into the globals once (`import "@tailor-platform/sdk/runtime/globals"` in a setup file, or list it in `tsconfig.json`'s `compilerOptions.types`), or call the typed wrappers from `@tailor-platform/sdk/runtime/*` instead. + For tighter alignment with the production runtime — Node.js module blocking, Web-only globals, and platform API mocks — pair the resolver helpers with the [`tailor-runtime` Vitest environment](#runtime-environment-emulation-beta) below. Three starter templates demonstrate the patterns below in a working project: @@ -249,8 +251,11 @@ test("mock encoding conversion", () => { ### Context Mock +`contextMock.setInvoker()` accepts the SDK-friendly shape — `attributes` is the attribute map and `attributeList` is the array of attribute IDs (matching `TailorUser` / `TailorActor`). Internally it is converted back to the raw platform shape, so resolver pipelines that read `tailor.context.getInvoker()` continue to receive the on-platform layout. + ```typescript import { contextMock } from "@tailor-platform/sdk/vitest"; +import { context } from "@tailor-platform/sdk/runtime"; beforeEach(() => contextMock.reset()); @@ -259,18 +264,20 @@ test("returns invoker information", () => { id: "f1e2d3c4-b5a6-4798-89a0-1b2c3d4e5f60", type: "machine_user", workspaceId: "b39bdd61-d442-4a4e-8599-33a78a4e19ab", + attributes: { role: "MANAGER" }, + attributeList: ["role"], }); - const invoker = tailor.context.getInvoker(); + const invoker = context.getInvoker(); expect(invoker?.type).toBe("machine_user"); + expect(invoker?.attributes).toEqual({ role: "MANAGER" }); expect(contextMock.calls).toHaveLength(1); }); test("anonymous caller", () => { contextMock.setInvoker(null); // null is the default - const invoker = tailor.context.getInvoker(); - expect(invoker).toBeNull(); + expect(context.getInvoker()).toBeNull(); }); ``` diff --git a/packages/sdk/src/runtime/__tests__/context.test.ts b/packages/sdk/src/runtime/__tests__/context.test.ts index 62b1dd878..b45be9841 100644 --- a/packages/sdk/src/runtime/__tests__/context.test.ts +++ b/packages/sdk/src/runtime/__tests__/context.test.ts @@ -3,21 +3,42 @@ */ import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; import * as context from "@/runtime/context"; -import { cleanupMocks, injectMocks } from "@/vitest/mock"; +import { cleanupMocks, contextMock, injectMocks } from "@/vitest/mock"; describe("@tailor-platform/sdk/runtime/context", () => { beforeEach(() => { injectMocks(globalThis); + contextMock.reset(); }); afterEach(() => { cleanupMocks(globalThis); }); - test("getInvoker forwards to global and returns Invoker | null", () => { + test("getInvoker returns null for anonymous invocations", () => { const result = context.getInvoker(); expectTypeOf(result).toEqualTypeOf(); expect(result).toBeNull(); }); + + test("getInvoker exposes SDK shape (attributes map + attributeList array)", () => { + contextMock.setInvoker({ + id: "u-1", + type: "machine_user", + workspaceId: "ws-1", + attributes: { role: "MANAGER" }, + attributeList: ["role"], + }); + + const invoker = context.getInvoker(); + + expect(invoker).toEqual({ + id: "u-1", + type: "machine_user", + workspaceId: "ws-1", + attributes: { role: "MANAGER" }, + attributeList: ["role"], + }); + }); }); diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index 7501c60c3..2d721b2ef 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -4,25 +4,56 @@ * Thin typed wrapper around the platform-provided `tailor.context` runtime API. * At runtime this delegates to `globalThis.tailor.context`. Use `contextMock` * from `@tailor-platform/sdk/vitest` to mock these calls in unit tests. + * + * The platform's raw `tailor.context.getInvoker()` returns + * `{ attributes: string[]; attributeMap: Record }`. This wrapper + * normalizes that into the SDK-friendly shape used by `TailorUser` and + * `TailorActor` (`attributes` is the attribute map, `attributeList` is the array + * of attribute IDs), so code touching invokers stays uniform across the SDK. * @example * import { context } from "@tailor-platform/sdk/runtime"; * * const invoker = context.getInvoker(); * if (invoker) { - * console.log(invoker.id, invoker.type); + * console.log(invoker.id, invoker.type, invoker.attributes, invoker.attributeList); * } */ -import { runtime, type ContextInvoker } from "./_runtime"; +import { runtime } from "./_runtime"; -/** Information about the invoker of the current function execution. */ -export type Invoker = ContextInvoker; +/** + * Information about the invoker of the current function execution. + * + * Uses the SDK-friendly shape — `attributes` is the attribute map and + * `attributeList` is the array of attribute IDs. This matches `TailorUser` + * and `TailorActor`. + */ +export interface Invoker { + /** The invoker's ID */ + id: string; + /** The invoker's type */ + type: "user" | "machine_user"; + /** The workspace ID */ + workspaceId: string; + /** A map of the invoker's attributes */ + attributes: Record; + /** The list of attribute IDs */ + attributeList: string[]; +} /** * Returns information about the invoker of the current function execution, * or `null` for anonymous invocations. - * @returns Invoker details, or `null` when the call is anonymous + * @returns Invoker details (SDK shape), or `null` when the call is anonymous */ export function getInvoker(): Invoker | null { - return runtime.tailor.context.getInvoker(); + const raw = runtime.tailor.context.getInvoker(); + if (!raw) return null; + return { + id: raw.id, + type: raw.type, + workspaceId: raw.workspaceId, + attributes: raw.attributeMap, + attributeList: raw.attributes, + }; } diff --git a/packages/sdk/src/vitest/mock.ts b/packages/sdk/src/vitest/mock.ts index d8baf93b0..0ac82d5fa 100644 --- a/packages/sdk/src/vitest/mock.ts +++ b/packages/sdk/src/vitest/mock.ts @@ -7,6 +7,7 @@ */ import type { ContextInvoker, IdpUser } from "../runtime/_runtime"; +import type { Invoker } from "../runtime/context"; // --------------------------------------------------------------------------- // Types @@ -425,10 +426,24 @@ export const contextMock = { /** * Set the invoker returned by `tailor.context.getInvoker()`. Pass `null` to * simulate an anonymous (unauthenticated) caller — the default. - * @param invoker - Invoker to return, or `null` for anonymous + * + * Accepts the SDK-friendly shape (`attributes` map + `attributeList` array) + * to match `TailorUser` / `TailorActor`. Internally this is converted back to + * the raw platform shape (`attributeMap` + `attributes` array) so the value + * surfaced by the ambient `tailor.context.getInvoker()` stays compatible with + * the resolver pipeline's invoker transform. + * @param invoker - Invoker to return (SDK shape), or `null` for anonymous */ - setInvoker(invoker: ContextInvoker | null): void { - getState().invoker = invoker; + setInvoker(invoker: Invoker | null): void { + getState().invoker = invoker + ? { + id: invoker.id, + type: invoker.type, + workspaceId: invoker.workspaceId, + attributes: invoker.attributeList, + attributeMap: invoker.attributes, + } + : null; }, get calls(): ContextCall[] { From 997e0b0664e757c33ad3329e8da63770f41d1b1c Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 13 May 2026 16:21:05 +0900 Subject: [PATCH 08/35] refactor(sdk): align runtime types and file stream mock with platform contract Address Copilot review findings on the new @tailor-platform/sdk/runtime entry: - docs/runtime.md: switch the subpath import example to the actual subpath (@tailor-platform/sdk/runtime/iconv) so it matches the section header, and reword the file.delete description so the implementation/export direction reads naturally. - runtime/globals.ts: declare the TailorDBFileError constructor signature with TailorDBFileErrorCode so `new TailorDBFileError(message, code?, cause?)` type-checks against the ambient declaration. - vitest/mock.ts: drop the Uint8Array auto-wrap shorthand from the openDownloadStream mock and reject raw bytes with a helpful TypeError. The platform stream protocol emits structured StreamValue items, so tests must enqueue an iterable of StreamValue to mirror that contract. Tighten the mock TailorDBFileError class signature to TailorDBFileErrorCode in lockstep. - runtime/__tests__/file.test.ts, vitest/__tests__/mock.test.ts: update the openDownloadStream tests to enqueue structured StreamValue sequences and cover the new guard against raw byte input. --- packages/sdk/docs/runtime.md | 4 +- .../sdk/src/runtime/__tests__/file.test.ts | 17 +++++++-- packages/sdk/src/runtime/globals.ts | 1 + .../sdk/src/vitest/__tests__/mock.test.ts | 38 +++++++++---------- packages/sdk/src/vitest/mock.ts | 28 ++++++++------ 5 files changed, 50 insertions(+), 38 deletions(-) diff --git a/packages/sdk/docs/runtime.md b/packages/sdk/docs/runtime.md index d45215bf4..b4c9697ea 100644 --- a/packages/sdk/docs/runtime.md +++ b/packages/sdk/docs/runtime.md @@ -42,7 +42,7 @@ const { metadata } = await file.upload("my-namespace", "Document", "attachment", Each namespace can also be imported individually so you only pull what you need: ```ts -import { iconv } from "@tailor-platform/sdk/runtime"; +import * as iconv from "@tailor-platform/sdk/runtime/iconv"; import type { ListUsersResponse, ClientConfig } from "@tailor-platform/sdk/runtime/idp"; ``` @@ -123,7 +123,7 @@ Pass the `names` argument as a `const` tuple to narrow the result keys: `getSecr ### `file` -`tailordb.file` BLOB API. The exported `delete` function is renamed from `deleteFile` to avoid the reserved keyword. +`tailordb.file` BLOB API. Internally implemented as `deleteFile` to avoid the reserved keyword; exported as `delete`. | Function | Description | | -------------------- | -------------------------------------------------- | diff --git a/packages/sdk/src/runtime/__tests__/file.test.ts b/packages/sdk/src/runtime/__tests__/file.test.ts index f485a5ca9..42899d0a6 100644 --- a/packages/sdk/src/runtime/__tests__/file.test.ts +++ b/packages/sdk/src/runtime/__tests__/file.test.ts @@ -94,17 +94,26 @@ describe("@tailor-platform/sdk/runtime/file", () => { ]); }); - test("openDownloadStream forwards and yields chunks", async () => { - fileMock.enqueueResult([new Uint8Array([1]), new Uint8Array([2])]); + test("openDownloadStream forwards and yields StreamValue chunks", async () => { + const sequence: file.StreamValue[] = [ + { + type: "metadata", + metadata: { contentType: "application/octet-stream", fileSize: 2, sha256sum: "h" }, + }, + { type: "chunk", data: new Uint8Array([1]), position: 0 }, + { type: "chunk", data: new Uint8Array([2]), position: 1 }, + { type: "complete" }, + ]; + fileMock.enqueueResult(sequence); const stream = await file.openDownloadStream("ns", "Doc", "blob", "rec-1"); - const chunks: unknown[] = []; + const chunks: file.StreamValue[] = []; for await (const chunk of stream) { chunks.push(chunk); } - expect(chunks).toEqual([new Uint8Array([1]), new Uint8Array([2])]); + expect(chunks).toEqual(sequence); expect(fileMock.calls[0]?.method).toBe("openDownloadStream"); }); diff --git a/packages/sdk/src/runtime/globals.ts b/packages/sdk/src/runtime/globals.ts index 49cf260bf..8c3e755d4 100644 --- a/packages/sdk/src/runtime/globals.ts +++ b/packages/sdk/src/runtime/globals.ts @@ -145,6 +145,7 @@ declare global { /** Custom error class for TailorDB File operations. */ class TailorDBFileError extends Error { + constructor(message: string, code?: TailorDBFileErrorCode, cause?: unknown); name: "TailorDBFileError"; code?: TailorDBFileErrorCode; cause?: unknown; diff --git a/packages/sdk/src/vitest/__tests__/mock.test.ts b/packages/sdk/src/vitest/__tests__/mock.test.ts index 24416584a..6f57b2a81 100644 --- a/packages/sdk/src/vitest/__tests__/mock.test.ts +++ b/packages/sdk/src/vitest/__tests__/mock.test.ts @@ -406,28 +406,24 @@ describe("mock", () => { expect(fileMock.calls).toHaveLength(0); }); - test("openDownloadStream with Uint8Array enqueued yields single chunk", async () => { - // Uint8Array is iterable as numbers; the mock must wrap it as one - // binary chunk instead of iterating byte-by-byte. - const bytes = new Uint8Array([1, 2, 3]); - fileMock.enqueueResult(bytes); - const stream = await (globalThis as any).tailordb.file.openDownloadStream( - "ns", - "T", - "f", - "r", - ); - const chunks: unknown[] = []; - for await (const chunk of stream) chunks.push(chunk); - expect(chunks).toHaveLength(1); - expect(chunks[0]).toBeInstanceOf(Uint8Array); - expect(chunks[0]).toEqual(bytes); + test("openDownloadStream rejects raw bytes to guide callers to structured chunks", async () => { + fileMock.enqueueResult(new Uint8Array([1, 2, 3])); + await expect( + (globalThis as any).tailordb.file.openDownloadStream("ns", "T", "f", "r"), + ).rejects.toThrow(/iterable of StreamValue items/); }); - test("openDownloadStream with array of Uint8Array yields chunks in order", async () => { - const a = new Uint8Array([1, 2]); - const b = new Uint8Array([3, 4]); - fileMock.enqueueResult([a, b]); + test("openDownloadStream yields the enqueued StreamValue sequence", async () => { + const bytes = new Uint8Array([1, 2, 3]); + const sequence = [ + { + type: "metadata" as const, + metadata: { contentType: "application/octet-stream", fileSize: 3, sha256sum: "h" }, + }, + { type: "chunk" as const, data: bytes, position: 0 }, + { type: "complete" as const }, + ]; + fileMock.enqueueResult(sequence); const stream = await (globalThis as any).tailordb.file.openDownloadStream( "ns", "T", @@ -436,7 +432,7 @@ describe("mock", () => { ); const chunks: unknown[] = []; for await (const chunk of stream) chunks.push(chunk); - expect(chunks).toEqual([a, b]); + expect(chunks).toEqual(sequence); }); test("default fallback is cloned so test mutations cannot leak across tests", async () => { diff --git a/packages/sdk/src/vitest/mock.ts b/packages/sdk/src/vitest/mock.ts index 0ac82d5fa..5105c586c 100644 --- a/packages/sdk/src/vitest/mock.ts +++ b/packages/sdk/src/vitest/mock.ts @@ -6,7 +6,7 @@ * responses and assert on recorded calls via the exported mock objects. */ -import type { ContextInvoker, IdpUser } from "../runtime/_runtime"; +import type { ContextInvoker, IdpUser, TailorDBFileErrorCode } from "../runtime/_runtime"; import type { Invoker } from "../runtime/context"; // --------------------------------------------------------------------------- @@ -1037,7 +1037,7 @@ const mockTailordbFile = { ReturnType >; }, - openDownloadStream( + async openDownloadStream( namespace: string, typeName: string, fieldName: string, @@ -1050,7 +1050,7 @@ const mockTailordbFile = { fieldName, recordId, ); - return Promise.resolve(toFileStream(resolved)); + return toFileStream(resolved); }, }; @@ -1066,15 +1066,21 @@ function toFileStream(value: unknown): FileStream { ) { return value as FileStream; } - // Binary chunk shorthand: a single ArrayBuffer / TypedArray (e.g. Uint8Array) - // should be delivered as one chunk, not iterated as a sequence of numbers. - // Tests passing `[chunk1, chunk2]` continue to work via the iterable branch - // below. + // Guard against passing raw bytes directly: `Uint8Array` is iterable as + // numbers, which would silently yield byte values as chunks. The platform's + // stream protocol emits structured `StreamValue` items, so callers must + // enqueue an iterable of `StreamValue` instead. if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) { - return toFileStream([value]); + throw new TypeError( + "fileMock.openDownloadStream expects an iterable of StreamValue items " + + '(e.g. [{ type: "chunk", data, position }, { type: "complete" }]); ' + + "got raw bytes. Wrap the bytes in a structured chunk first.", + ); } // Iterable (array, sync iterator, etc.): wrap as a chunked async iterator - // so `fileMock.enqueueResult([chunk1, chunk2])` controls stream contents. + // so `fileMock.enqueueResult([{ type: "metadata", ... }, { type: "chunk", ... }, ...])` + // controls stream contents. The platform emits structured StreamValue items; + // tests should enqueue an iterable of StreamValue to mirror that contract. if ( value !== null && typeof value === "object" && @@ -1151,10 +1157,10 @@ class TailorErrorMessageMock extends Error { } class TailorDBFileErrorMock extends Error { - code?: string; + code?: TailorDBFileErrorCode; override cause: unknown; - constructor(message: string, code?: string, cause?: unknown) { + constructor(message: string, code?: TailorDBFileErrorCode, cause?: unknown) { super(message); this.name = "TailorDBFileError"; this.code = code; From b407fa243b08d5078e9d0872b03755277b055c1b Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 13 May 2026 16:37:11 +0900 Subject: [PATCH 09/35] docs(sdk): drop runtime sequencing detail from user-facing workflow docs The synchronous-execution and Promise.all wording describes internal runtime behavior that SDK users do not need to reason about. Removing it from the runtime workflow JSDoc, runtime API table, and workflow guide leaves the surface easier to read. --- packages/sdk/docs/runtime.md | 12 ++++++------ packages/sdk/docs/services/workflow.md | 5 ----- packages/sdk/src/runtime/workflow.ts | 3 --- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/sdk/docs/runtime.md b/packages/sdk/docs/runtime.md index b4c9697ea..accce9ea1 100644 --- a/packages/sdk/docs/runtime.md +++ b/packages/sdk/docs/runtime.md @@ -108,12 +108,12 @@ Pass the `names` argument as a `const` tuple to narrow the result keys: `getSecr ### `workflow` -| Function | Description | -| -------------------- | ------------------------------------------------- | -| `triggerWorkflow` | Trigger a workflow and return its execution ID | -| `triggerJobFunction` | Synchronously trigger a job and return its result | -| `wait` | Suspend a job at a wait point | -| `resolve` | Resolve a wait point on a running execution | +| Function | Description | +| -------------------- | ---------------------------------------------- | +| `triggerWorkflow` | Trigger a workflow and return its execution ID | +| `triggerJobFunction` | Trigger a job and return its result | +| `wait` | Suspend a job at a wait point | +| `resolve` | Resolve a wait point on a running execution | ### `context` diff --git a/packages/sdk/docs/services/workflow.md b/packages/sdk/docs/services/workflow.md index 702f1e2f4..022c963d4 100644 --- a/packages/sdk/docs/services/workflow.md +++ b/packages/sdk/docs/services/workflow.md @@ -106,9 +106,6 @@ import { sendNotification } from "./jobs/send-notification"; export const mainJob = createWorkflowJob({ name: "main-job", body: async (input: { customerId: string }) => { - // You can write `await` for type-safety in your source. - // During deployment bundling, job.trigger() calls are transformed to a synchronous - // runtime call and `await` is removed. const customer = await fetchCustomer.trigger({ customerId: input.customerId, }); @@ -121,8 +118,6 @@ export const mainJob = createWorkflowJob({ }); ``` -**Important:** On the Tailor runtime, job triggers are executed synchronously. This means `Promise.all([jobA.trigger(), jobB.trigger()])` will not run jobs in parallel. - ### Deterministic Execution Requirement Workflow jobs use a **suspend/resume execution model**. When a job calls `.trigger()`, the runtime suspends the current job, executes the triggered job, and then **re-executes the calling job from the beginning** with cached results from previous triggers. diff --git a/packages/sdk/src/runtime/workflow.ts b/packages/sdk/src/runtime/workflow.ts index fa40c8db9..1803a9681 100644 --- a/packages/sdk/src/runtime/workflow.ts +++ b/packages/sdk/src/runtime/workflow.ts @@ -36,9 +36,6 @@ export function triggerWorkflow( /** * Triggers a job function and returns its result. - * - * The TypeScript signature returns `any` to mirror the platform contract; the - * underlying call is synchronous on the server but `Promise`-based in the API. * @param job_name - Job name as defined in the workflow * @param args - Arguments forwarded to the job * @returns The job's return value From 362c95813f04944e3cdb9171041b3f8ed717f0d3 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 13 May 2026 20:06:11 +0900 Subject: [PATCH 10/35] fix(sdk): broaden runtime globals and tighten file stream mock contract - Declare TailorErrors and TailorErrorMessage in @tailor-platform/sdk/runtime/globals so consumers opting into the ambient globals can reference the runtime-injected error classes the same way they already reference TailorDBFileError. - Use a valid TailorDBFileErrorCode literal (OPERATION_FAILED) in the file runtime test instead of an out-of-union string. - Validate each item yielded by fileMock.openDownloadStream and reject non StreamValue elements (raw bytes, primitives, untyped objects). Previously the mock only rejected a raw Uint8Array passed at the top level, so iterables like Uint8Array[] silently produced non-conforming streams. --- .../sdk/src/runtime/__tests__/file.test.ts | 9 ++++--- packages/sdk/src/runtime/globals.ts | 25 ++++++++++++++++++ .../sdk/src/vitest/__tests__/mock.test.ts | 12 +++++++++ packages/sdk/src/vitest/mock.ts | 26 +++++++++++++++++++ 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/packages/sdk/src/runtime/__tests__/file.test.ts b/packages/sdk/src/runtime/__tests__/file.test.ts index 42899d0a6..bdc66324d 100644 --- a/packages/sdk/src/runtime/__tests__/file.test.ts +++ b/packages/sdk/src/runtime/__tests__/file.test.ts @@ -120,12 +120,15 @@ describe("@tailor-platform/sdk/runtime/file", () => { test("TailorDBFileError type alias resolves to globalThis class", () => { const TailorDBFileError = ( globalThis as unknown as { - TailorDBFileError: new (m: string, c?: string) => Error & { code?: string }; + TailorDBFileError: new ( + m: string, + c?: file.TailorDBFileErrorCode, + ) => Error & { code?: file.TailorDBFileErrorCode }; } ).TailorDBFileError; - const err = new TailorDBFileError("not found", "NOT_FOUND"); + const err = new TailorDBFileError("operation failed", "OPERATION_FAILED"); expect(err.name).toBe("TailorDBFileError"); - expect(err.code).toBe("NOT_FOUND"); + expect(err.code).toBe("OPERATION_FAILED"); // Type-level: file.TailorDBFileError is the global class const _typed: file.TailorDBFileError = err as file.TailorDBFileError; expect(_typed).toBe(err); diff --git a/packages/sdk/src/runtime/globals.ts b/packages/sdk/src/runtime/globals.ts index 8c3e755d4..51387000c 100644 --- a/packages/sdk/src/runtime/globals.ts +++ b/packages/sdk/src/runtime/globals.ts @@ -151,6 +151,31 @@ declare global { cause?: unknown; } + /** Individual error entry attached to {@link TailorErrors}. */ + interface TailorErrorItem { + message: string; + path: (string | number)[]; + } + + /** + * Aggregate validation error raised by the Tailor Platform Function runtime. + * The runtime serializes the items into the `message` (`"TailorErrors: {...}"`) + * and also exposes them on `.errors`. + */ + class TailorErrors extends Error { + constructor(errors: TailorErrorItem[]); + name: "TailorErrors"; + errors: TailorErrorItem[]; + } + + /** + * Single-message error raised by the Tailor Platform Function runtime. + */ + class TailorErrorMessage extends Error { + constructor(message: string); + name: "TailorErrorMessage"; + } + namespace tailor.idp { type ClientConfig = IdpClientConfig; type User = IdpUser; diff --git a/packages/sdk/src/vitest/__tests__/mock.test.ts b/packages/sdk/src/vitest/__tests__/mock.test.ts index 6f57b2a81..8af410579 100644 --- a/packages/sdk/src/vitest/__tests__/mock.test.ts +++ b/packages/sdk/src/vitest/__tests__/mock.test.ts @@ -413,6 +413,18 @@ describe("mock", () => { ).rejects.toThrow(/iterable of StreamValue items/); }); + test("openDownloadStream rejects non-StreamValue elements yielded by the iterable", async () => { + // Uint8Array[] is iterable but its elements aren't StreamValue items. + fileMock.enqueueResult([new Uint8Array([1]), new Uint8Array([2])]); + const stream = await (globalThis as any).tailordb.file.openDownloadStream( + "ns", + "T", + "f", + "r", + ); + await expect(stream.next()).rejects.toThrow(/StreamValue/); + }); + test("openDownloadStream yields the enqueued StreamValue sequence", async () => { const bytes = new Uint8Array([1, 2, 3]); const sequence = [ diff --git a/packages/sdk/src/vitest/mock.ts b/packages/sdk/src/vitest/mock.ts index 5105c586c..0a14386c3 100644 --- a/packages/sdk/src/vitest/mock.ts +++ b/packages/sdk/src/vitest/mock.ts @@ -1094,6 +1094,9 @@ function toFileStream(value: unknown): FileStream { const stream: FileStream = { async next() { const r = await inner.next(); + if (!r.done) { + assertStreamValue(r.value); + } return r.done ? { done: true as const, value: undefined } : r; }, async close() {}, @@ -1115,6 +1118,29 @@ function toFileStream(value: unknown): FileStream { return empty; } +function assertStreamValue(v: unknown): void { + if (v === null || typeof v !== "object") { + throw new TypeError( + 'fileMock.openDownloadStream expected a StreamValue item ({ type: "metadata" | "chunk" | "complete", ... }); ' + + `got ${typeof v === "object" ? "null" : typeof v}.`, + ); + } + // ArrayBuffer / TypedArray are objects but never valid StreamValue items. + if (v instanceof ArrayBuffer || ArrayBuffer.isView(v)) { + throw new TypeError( + "fileMock.openDownloadStream expected a StreamValue item, got raw bytes. " + + 'Wrap the bytes in a structured chunk first (e.g. { type: "chunk", data, position }).', + ); + } + const type = (v as { type?: unknown }).type; + if (type !== "metadata" && type !== "chunk" && type !== "complete") { + throw new TypeError( + 'fileMock.openDownloadStream expected a StreamValue item with type "metadata" | "chunk" | "complete"; ' + + `got ${JSON.stringify(type)}.`, + ); + } +} + // --------------------------------------------------------------------------- // Error class mocks // --------------------------------------------------------------------------- From 8d6cebb781c60fa9a93570ffe8a91a226c12d8cd Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 13 May 2026 21:06:55 +0900 Subject: [PATCH 11/35] fix(sdk): keep ambient runtime globals active when importing @tailor-platform/sdk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-export `runtime/globals` from the configure entry so that consumers who only `import` from `@tailor-platform/sdk` keep getting the `tailor.*` / `tailordb.*` ambient types — preserving the previous `@tailor-platform/function-types`-based behavior. To be removed in v2.0. --- .changeset/runtime-wrapper.md | 2 +- packages/sdk/docs/runtime.md | 4 +++- packages/sdk/src/configure/index.ts | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.changeset/runtime-wrapper.md b/.changeset/runtime-wrapper.md index 37653a3da..b2c8ced20 100644 --- a/.changeset/runtime-wrapper.md +++ b/.changeset/runtime-wrapper.md @@ -13,4 +13,4 @@ const client = new idp.Client({ namespace: "my-namespace" }); const { metadata } = await file.upload("ns", "Document", "attachment", recordId, bytes); ``` -The SDK no longer depends on the external `@tailor-platform/function-types` package; its declarations are now vendored inside the SDK. If you still want unqualified `tailor.iconv.convert(...)` / `new tailordb.Client(...)` calls to type-check, opt into the globals by adding a side-effect `import "@tailor-platform/sdk/runtime/globals"` or by listing it in `tsconfig.json`'s `compilerOptions.types`. +The SDK no longer depends on the external `@tailor-platform/function-types` package; its declarations are now vendored inside the SDK. For backwards compatibility the ambient `tailor.*` / `tailordb.*` types are still activated automatically when you import from `@tailor-platform/sdk`, so existing code keeps type-checking with no changes. This implicit activation will be removed in v2.0 — new code is encouraged to use the typed wrappers from `@tailor-platform/sdk/runtime`, or to opt into the globals explicitly via `import "@tailor-platform/sdk/runtime/globals"` (or by listing the entry in `tsconfig.json`'s `compilerOptions.types`). diff --git a/packages/sdk/docs/runtime.md b/packages/sdk/docs/runtime.md index accce9ea1..ecd4f71e5 100644 --- a/packages/sdk/docs/runtime.md +++ b/packages/sdk/docs/runtime.md @@ -52,7 +52,9 @@ import type { ListUsersResponse, ClientConfig } from "@tailor-platform/sdk/runti Most users do not need to touch the globals entry — `@tailor-platform/sdk/runtime` (and its subpath modules) cover the same surface without depending on any ambient declaration. -If you do want unqualified calls like `tailor.iconv.convert(...)` and `new tailordb.Client(...)` to type-check, opt in by adding a single side-effect import anywhere in your project: +For backwards compatibility with the previous `@tailor-platform/function-types`-based setup, the SDK still activates the ambient `tailor.*` / `tailordb.*` types automatically when you import from `@tailor-platform/sdk`. **This implicit activation will be removed in v2.0**; new code should prefer the typed wrappers from `@tailor-platform/sdk/runtime`. + +If you want to opt into the globals explicitly (or you are migrating ahead of v2.0), add a single side-effect import anywhere in your project: ```ts import "@tailor-platform/sdk/runtime/globals"; diff --git a/packages/sdk/src/configure/index.ts b/packages/sdk/src/configure/index.ts index 9147c5dcc..0a252d795 100644 --- a/packages/sdk/src/configure/index.ts +++ b/packages/sdk/src/configure/index.ts @@ -1,3 +1,6 @@ +// Activates the legacy `tailor.*` / `tailordb.*` ambient globals. Remove in v2.0. +export * from "@/runtime/globals"; + import { t as _t } from "@/configure/types"; import type * as helperTypes from "@/types/helpers"; From e5e0872029cceb7266985d641f9869a25d4d4968 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 13 May 2026 21:54:02 +0900 Subject: [PATCH 12/35] chore(sdk): exclude __test_fixtures__ from tsconfig and consolidate lint ignore Test fixtures under __test_fixtures__ are inputs for CLI command tests, not SDK source. Including them in tsconfig pulled dist/*.d.mts back into the program (via their `declare module "@tailor-platform/sdk"`), which clashed with src/runtime/globals.ts under tsc as TS6200. Excluding the fixtures removes the conflict; the ESLint global ignore is simplified to match. --- packages/sdk/eslint.config.js | 3 +-- packages/sdk/tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/sdk/eslint.config.js b/packages/sdk/eslint.config.js index dbcd22b0c..d18a11227 100644 --- a/packages/sdk/eslint.config.js +++ b/packages/sdk/eslint.config.js @@ -24,8 +24,7 @@ export default defineConfig([ ".tailor-sdk/", "tailor.d.ts", "plugin-defined.d.ts", - "**/__test_fixtures__/dist/", - "**/__test_fixtures__/*-compat-out/", + "**/__test_fixtures__/", ]), eslint.configs.recommended, tseslint.configs.recommended, diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index fe0497ed1..b532c8053 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -20,5 +20,5 @@ "./vitest.config.ts", "./zinfer.config.ts" ], - "exclude": ["node_modules", "dist", "e2e/fixtures"] + "exclude": ["node_modules", "dist", "e2e/fixtures", "**/__test_fixtures__/**"] } From 7cf33284b9ea2981431337f1b22b668ae6cb3c0b Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 13 May 2026 22:15:36 +0900 Subject: [PATCH 13/35] chore(sdk): address Copilot review feedback on runtime/vitest mocks - changeset: note new contextMock and the fileMock.openDownloadStream breaking change (raw Uint8Array/ArrayBuffer no longer accepted; enqueue a structured StreamValue iterable instead). - testing.md: add a worked example showing how to enqueue metadata/chunk/complete StreamValue items for stream tests. - vitest/mock.ts: split the misleading "SecretManager Mock" header so contextMock sits under its own "Context Mock" section. - example fixtures: regenerate expected output via test:generator:update-expects so files.ts reflects the new @tailor-platform/sdk/runtime/file imports (User.schema.ts foreignKey drift is picked up as a side effect of the regeneration). --- .changeset/runtime-wrapper.md | 5 +++++ example/tests/fixtures/expected/files.ts | 18 +++++++++++----- .../expected/seed/data/User.schema.ts | 3 +++ packages/sdk/docs/testing.md | 21 +++++++++++++++++++ packages/sdk/src/vitest/mock.ts | 6 +++++- 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/.changeset/runtime-wrapper.md b/.changeset/runtime-wrapper.md index b2c8ced20..bcf23c510 100644 --- a/.changeset/runtime-wrapper.md +++ b/.changeset/runtime-wrapper.md @@ -14,3 +14,8 @@ const { metadata } = await file.upload("ns", "Document", "attachment", recordId, ``` The SDK no longer depends on the external `@tailor-platform/function-types` package; its declarations are now vendored inside the SDK. For backwards compatibility the ambient `tailor.*` / `tailordb.*` types are still activated automatically when you import from `@tailor-platform/sdk`, so existing code keeps type-checking with no changes. This implicit activation will be removed in v2.0 — new code is encouraged to use the typed wrappers from `@tailor-platform/sdk/runtime`, or to opt into the globals explicitly via `import "@tailor-platform/sdk/runtime/globals"` (or by listing the entry in `tsconfig.json`'s `compilerOptions.types`). + +Other test-mock changes from `@tailor-platform/sdk/vitest`: + +- New `contextMock` — control the invoker returned by `tailor.context.getInvoker()` and inspect call history. See the Testing Guide for usage. +- Breaking: `fileMock.enqueueResult(...)` now rejects raw `Uint8Array` / `ArrayBuffer` payloads for `openDownloadStream`. Enqueue a structured iterable of `StreamValue` items (`{ type: "metadata" }`, `{ type: "chunk", data, position }`, `{ type: "complete" }`) so test streams stay aligned with the platform's structured stream contract. The shorthand `Uint8Array` enqueue is still accepted by `download` / `downloadAsBase64`. diff --git a/example/tests/fixtures/expected/files.ts b/example/tests/fixtures/expected/files.ts index 052806afd..0389b9acd 100644 --- a/example/tests/fixtures/expected/files.ts +++ b/example/tests/fixtures/expected/files.ts @@ -1,3 +1,11 @@ +import * as file from "@tailor-platform/sdk/runtime/file"; +import type { + FileUploadOptions, + FileUploadResponse, + FileMetadata, + FileStreamIterator, +} from "@tailor-platform/sdk/runtime/file"; + export interface TypeWithFiles { SalesOrder: { fields: "receipt" | "form"; @@ -21,7 +29,7 @@ export async function downloadFile( field: TypeWithFiles[T]["fields"], recordId: string, ) { - return await tailordb.file.download(namespaces[type], type, field, recordId); + return await file.download(namespaces[type], type, field, recordId); } export async function uploadFile( @@ -31,7 +39,7 @@ export async function uploadFile( data: string | ArrayBuffer | Uint8Array | number[], options?: FileUploadOptions, ): Promise { - return await tailordb.file.upload(namespaces[type], type, field, recordId, data, options); + return await file.upload(namespaces[type], type, field, recordId, data, options); } export async function deleteFile( @@ -39,7 +47,7 @@ export async function deleteFile( field: TypeWithFiles[T]["fields"], recordId: string, ): Promise { - return await tailordb.file.delete(namespaces[type], type, field, recordId); + return await file.delete(namespaces[type], type, field, recordId); } export async function getFileMetadata( @@ -47,7 +55,7 @@ export async function getFileMetadata( field: TypeWithFiles[T]["fields"], recordId: string, ): Promise { - return await tailordb.file.getMetadata(namespaces[type], type, field, recordId); + return await file.getMetadata(namespaces[type], type, field, recordId); } export async function openFileDownloadStream( @@ -55,5 +63,5 @@ export async function openFileDownloadStream( field: TypeWithFiles[T]["fields"], recordId: string, ): Promise { - return await tailordb.file.openDownloadStream(namespaces[type], type, field, recordId); + return await file.openDownloadStream(namespaces[type], type, field, recordId); } diff --git a/example/tests/fixtures/expected/seed/data/User.schema.ts b/example/tests/fixtures/expected/seed/data/User.schema.ts index 5c400f2a0..4c9d24707 100644 --- a/example/tests/fixtures/expected/seed/data/User.schema.ts +++ b/example/tests/fixtures/expected/seed/data/User.schema.ts @@ -13,6 +13,9 @@ const hook = createTailorDBHook(user); export const schema = defineSchema( createStandardSchema(schemaType, hook), { + foreignKeys: [ + {"column":"email","references":{"table":"_User","column":"name"}}, + ], indexes: [ {"name":"user_email_unique_idx","columns":["email"],"unique":true}, {"name":"idx_name_department","columns":["name","department"],"unique":false}, diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index ddd725b11..63452ba72 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -230,6 +230,27 @@ test("mock file download", async () => { }); ``` +For `openDownloadStream`, enqueue an iterable of `StreamValue` items — `metadata`, one or more `chunk` items, and a terminal `complete` (or `error`). Raw `Uint8Array` / `ArrayBuffer` chunks are rejected so tests stay aligned with the platform's structured stream contract. + +```typescript +test("mock file download stream", async () => { + fileMock.enqueueResult([ + { + type: "metadata", + metadata: { contentType: "image/png", fileSize: 3, sha256sum: "abc", lastUploadedAt: "" }, + }, + { type: "chunk", data: new Uint8Array([1, 2]), position: 0 }, + { type: "chunk", data: new Uint8Array([3]), position: 2 }, + { type: "complete" }, + ]); + + const stream = await tailordb.file.openDownloadStream("ns", "Doc", "attachment", "r-1"); + const items = []; + for await (const item of stream) items.push(item); + expect(items).toHaveLength(4); +}); +``` + ### Iconv Mock ```typescript diff --git a/packages/sdk/src/vitest/mock.ts b/packages/sdk/src/vitest/mock.ts index 0a14386c3..683282433 100644 --- a/packages/sdk/src/vitest/mock.ts +++ b/packages/sdk/src/vitest/mock.ts @@ -418,7 +418,7 @@ export const workflowMock = { }; // --------------------------------------------------------------------------- -// SecretManager Mock +// Context Mock // --------------------------------------------------------------------------- /** Mock control for `tailor.context` — invoker store and call recording. */ @@ -457,6 +457,10 @@ export const contextMock = { }, }; +// --------------------------------------------------------------------------- +// SecretManager Mock +// --------------------------------------------------------------------------- + /** Mock control for `tailor.secretmanager` — secret store and call recording. */ export const secretmanagerMock = { setSecrets(secrets: Record>): void { From f8a67817dc88435a9124cf3e9313249cb24cf97c Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 13 May 2026 22:30:27 +0900 Subject: [PATCH 14/35] docs(sdk): align openDownloadStream mock example with StreamValue type Drop the non-existent "error" variant from the description and remove the `lastUploadedAt` field from the example's `StreamMetadata` payload so the snippet matches the runtime types exported from @tailor-platform/sdk/runtime/file. --- packages/sdk/docs/testing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index 63452ba72..04d8ea743 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -230,14 +230,14 @@ test("mock file download", async () => { }); ``` -For `openDownloadStream`, enqueue an iterable of `StreamValue` items — `metadata`, one or more `chunk` items, and a terminal `complete` (or `error`). Raw `Uint8Array` / `ArrayBuffer` chunks are rejected so tests stay aligned with the platform's structured stream contract. +For `openDownloadStream`, enqueue an iterable of `StreamValue` items — `metadata`, one or more `chunk` items, and a terminal `complete`. Raw `Uint8Array` / `ArrayBuffer` chunks are rejected so tests stay aligned with the platform's structured stream contract. ```typescript test("mock file download stream", async () => { fileMock.enqueueResult([ { type: "metadata", - metadata: { contentType: "image/png", fileSize: 3, sha256sum: "abc", lastUploadedAt: "" }, + metadata: { contentType: "image/png", fileSize: 3, sha256sum: "abc" }, }, { type: "chunk", data: new Uint8Array([1, 2]), position: 0 }, { type: "chunk", data: new Uint8Array([3]), position: 2 }, From 0d8f55d96ab3e57e714dc6895f8f2d433a37891c Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 13 May 2026 23:23:17 +0900 Subject: [PATCH 15/35] docs(sdk): drop wrapper-internal shape conversion from runtime docs The contextMock.setInvoker JSDoc, runtime/context.ts file JSDoc, the testing guide Context Mock section, and the runtime.md file table all described the platform-vs-SDK invoker shape conversion (and the deleteFile reserved-keyword workaround). Those are internal details that defeat the abstraction the wrappers exist to provide. Replace them with one-sentence behavior descriptions framed in the SDK-visible TailorUser / TailorActor shape. --- packages/sdk/docs/runtime.md | 2 +- packages/sdk/docs/testing.md | 2 +- packages/sdk/src/runtime/context.ts | 13 +++---------- packages/sdk/src/vitest/mock.ts | 12 +++--------- 4 files changed, 8 insertions(+), 21 deletions(-) diff --git a/packages/sdk/docs/runtime.md b/packages/sdk/docs/runtime.md index ecd4f71e5..a94ee18c2 100644 --- a/packages/sdk/docs/runtime.md +++ b/packages/sdk/docs/runtime.md @@ -125,7 +125,7 @@ Pass the `names` argument as a `const` tuple to narrow the result keys: `getSecr ### `file` -`tailordb.file` BLOB API. Internally implemented as `deleteFile` to avoid the reserved keyword; exported as `delete`. +`tailordb.file` BLOB API. | Function | Description | | -------------------- | -------------------------------------------------- | diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index 04d8ea743..8c7c6dfc5 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -272,7 +272,7 @@ test("mock encoding conversion", () => { ### Context Mock -`contextMock.setInvoker()` accepts the SDK-friendly shape — `attributes` is the attribute map and `attributeList` is the array of attribute IDs (matching `TailorUser` / `TailorActor`). Internally it is converted back to the raw platform shape, so resolver pipelines that read `tailor.context.getInvoker()` continue to receive the on-platform layout. +Pass the invoker shape that matches `TailorUser` / `TailorActor`: `attributes` is the attribute map and `attributeList` is the array of attribute IDs. Pass `null` for an anonymous caller (the default). ```typescript import { contextMock } from "@tailor-platform/sdk/vitest"; diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index 2d721b2ef..c103d397a 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -4,12 +4,6 @@ * Thin typed wrapper around the platform-provided `tailor.context` runtime API. * At runtime this delegates to `globalThis.tailor.context`. Use `contextMock` * from `@tailor-platform/sdk/vitest` to mock these calls in unit tests. - * - * The platform's raw `tailor.context.getInvoker()` returns - * `{ attributes: string[]; attributeMap: Record }`. This wrapper - * normalizes that into the SDK-friendly shape used by `TailorUser` and - * `TailorActor` (`attributes` is the attribute map, `attributeList` is the array - * of attribute IDs), so code touching invokers stays uniform across the SDK. * @example * import { context } from "@tailor-platform/sdk/runtime"; * @@ -24,9 +18,8 @@ import { runtime } from "./_runtime"; /** * Information about the invoker of the current function execution. * - * Uses the SDK-friendly shape — `attributes` is the attribute map and - * `attributeList` is the array of attribute IDs. This matches `TailorUser` - * and `TailorActor`. + * Matches the shape of `TailorUser` and `TailorActor` — `attributes` is the + * attribute map and `attributeList` is the array of attribute IDs. */ export interface Invoker { /** The invoker's ID */ @@ -44,7 +37,7 @@ export interface Invoker { /** * Returns information about the invoker of the current function execution, * or `null` for anonymous invocations. - * @returns Invoker details (SDK shape), or `null` when the call is anonymous + * @returns Invoker details, or `null` when the call is anonymous */ export function getInvoker(): Invoker | null { const raw = runtime.tailor.context.getInvoker(); diff --git a/packages/sdk/src/vitest/mock.ts b/packages/sdk/src/vitest/mock.ts index 683282433..c999b73be 100644 --- a/packages/sdk/src/vitest/mock.ts +++ b/packages/sdk/src/vitest/mock.ts @@ -424,15 +424,9 @@ export const workflowMock = { /** Mock control for `tailor.context` — invoker store and call recording. */ export const contextMock = { /** - * Set the invoker returned by `tailor.context.getInvoker()`. Pass `null` to - * simulate an anonymous (unauthenticated) caller — the default. - * - * Accepts the SDK-friendly shape (`attributes` map + `attributeList` array) - * to match `TailorUser` / `TailorActor`. Internally this is converted back to - * the raw platform shape (`attributeMap` + `attributes` array) so the value - * surfaced by the ambient `tailor.context.getInvoker()` stays compatible with - * the resolver pipeline's invoker transform. - * @param invoker - Invoker to return (SDK shape), or `null` for anonymous + * Set the invoker returned by `context.getInvoker()`. Pass `null` to simulate + * an anonymous (unauthenticated) caller — the default. + * @param invoker - Invoker to return, or `null` for anonymous */ setInvoker(invoker: Invoker | null): void { getState().invoker = invoker From ba1171f004c13f03f11df435b035738dc62dd0fa Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 14 May 2026 00:05:36 +0900 Subject: [PATCH 16/35] chore(example): revert unrelated User.schema fixture drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous regenerate-fixtures step picked up a foreignKey drift on example/tests/fixtures/expected/seed/data/User.schema.ts that is unrelated to the runtime-wrapper change in this PR (example/tailordb and the generator are both untouched on this branch). The fixture is not consumed by any test — keep main's snapshot here and handle the drift in a separate change. --- example/tests/fixtures/expected/seed/data/User.schema.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/example/tests/fixtures/expected/seed/data/User.schema.ts b/example/tests/fixtures/expected/seed/data/User.schema.ts index 4c9d24707..5c400f2a0 100644 --- a/example/tests/fixtures/expected/seed/data/User.schema.ts +++ b/example/tests/fixtures/expected/seed/data/User.schema.ts @@ -13,9 +13,6 @@ const hook = createTailorDBHook(user); export const schema = defineSchema( createStandardSchema(schemaType, hook), { - foreignKeys: [ - {"column":"email","references":{"table":"_User","column":"name"}}, - ], indexes: [ {"name":"user_email_unique_idx","columns":["email"],"unique":true}, {"name":"idx_name_department","columns":["name","department"],"unique":false}, From c391cdeb6705b8fee053036d4ee1d85d992c4bf8 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 14 May 2026 00:44:15 +0900 Subject: [PATCH 17/35] refactor(sdk): move runtime types into per-service wrappers Inline each service's types and platform API interface alongside its wrapper module, and rename _runtime.ts to internal.ts (containing only the lazy runtime accessor, Tailordb Client types, and the top-level TailorRuntime/TailordbRuntime aggregators). The underscore prefix was not a codebase convention, so the shared file is renamed for clarity. Platform API surfaces (TailorIconvAPI, TailorDBFileAPI, etc.) are marked @internal so consumers do not depend on them directly. --- packages/sdk/src/runtime/_runtime.ts | 423 --------------------- packages/sdk/src/runtime/authconnection.ts | 13 +- packages/sdk/src/runtime/context.ts | 29 +- packages/sdk/src/runtime/file.ts | 164 ++++++-- packages/sdk/src/runtime/globals.ts | 32 +- packages/sdk/src/runtime/iconv.ts | 40 +- packages/sdk/src/runtime/idp.ts | 111 +++++- packages/sdk/src/runtime/internal.ts | 103 +++++ packages/sdk/src/runtime/secretmanager.ts | 15 +- packages/sdk/src/runtime/workflow.ts | 39 +- packages/sdk/src/vitest/mock.ts | 5 +- 11 files changed, 474 insertions(+), 500 deletions(-) delete mode 100644 packages/sdk/src/runtime/_runtime.ts create mode 100644 packages/sdk/src/runtime/internal.ts diff --git a/packages/sdk/src/runtime/_runtime.ts b/packages/sdk/src/runtime/_runtime.ts deleted file mode 100644 index 1cfeb9e8b..000000000 --- a/packages/sdk/src/runtime/_runtime.ts +++ /dev/null @@ -1,423 +0,0 @@ -/** - * Internal runtime bindings shared by the typed wrappers in - * `@tailor-platform/sdk/runtime/*`. Not part of the public API. - * - * - The exported `runtime` value reads `tailor` / `tailordb` from `globalThis` - * lazily through getters so wrappers stay decoupled from module-load order - * (mocks injected in `beforeEach` are picked up on next access). - * - The exported module-scope types describe the platform runtime surface - * without introducing any ambient global declarations. The `declare global` - * block lives only in `./globals`, which callers opt into explicitly. - * @internal - */ - -/* eslint-disable @typescript-eslint/no-explicit-any */ - -// --------------------------------------------------------------------------- -// Module-scope data types -// --------------------------------------------------------------------------- - -// --- Tailordb ------------------------------------------------------------- - -/** Result of a single `queryObject` call against the TailorDB driver. */ -export interface TailordbQueryResult { - rows: T[]; - command: TailordbCommandType; - rowCount: number; -} - -/** SQL command type recorded on a {@link TailordbQueryResult}. */ -export type TailordbCommandType = - | "INSERT" - | "DELETE" - | "UPDATE" - | "SELECT" - | "MOVE" - | "FETCH" - | "COPY" - | "CREATE"; - -// --- TailorDB file API --------------------------------------------------- - -/** Upload response metadata. */ -export interface UploadMetadata { - fileSize: number; - sha256sum: string; -} - -/** Download response metadata. */ -export interface DownloadMetadata { - contentType: string; - fileSize: number; - sha256sum: string; - lastUploadedAt: string; -} - -/** File metadata (for `getMetadata`). */ -export interface FileMetadata { - contentType: string; - fileSize: number; - sha256sum: string; - urlPath: string; - lastUploadedAt?: string; -} - -/** Stream metadata (first chunk). */ -export interface StreamMetadata { - contentType: string; - fileSize: number; - sha256sum: string; -} - -/** Upload options. */ -export interface FileUploadOptions { - contentType?: string; -} - -/** Upload response. */ -export interface FileUploadResponse { - metadata: UploadMetadata; -} - -/** Download response. */ -export interface FileDownloadResponse { - data: Uint8Array; - metadata: DownloadMetadata; -} - -/** Download-as-Base64 response. */ -export interface FileDownloadAsBase64Response { - data: string; - metadata: DownloadMetadata; -} - -/** Stream chunk types. */ -export type StreamValue = - | { type: "metadata"; metadata: StreamMetadata } - | { type: "chunk"; data: Uint8Array; position: number } - | { type: "complete" }; - -/** Stream iterator interface. */ -export interface FileStreamIterator extends AsyncIterableIterator { - next(): Promise>; - close(): Promise; -} - -/** TailorDB File API surface. */ -export interface TailorDBFileAPI { - upload( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - data: string | ArrayBuffer | Uint8Array | number[], - options?: FileUploadOptions, - ): Promise; - - download( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - ): Promise; - - downloadAsBase64( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - ): Promise; - - delete(namespace: string, typeName: string, fieldName: string, recordId: string): Promise; - - getMetadata( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - ): Promise; - - openDownloadStream( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - ): Promise; -} - -/** Error code emitted by `TailorDBFileError`. */ -export type TailorDBFileErrorCode = - | "INVALID_PARAMS" - | "INVALID_DATA_TYPE" - | "OPERATION_FAILED" - | "DELETE_FAILED" - | "STREAM_OPEN_FAILED" - | "STREAM_READ_ERROR" - | "STREAM_ERROR" - | "FILE_TOO_LARGE"; - -/** - * Type-only shape of the `TailorDBFileError` runtime class. The class itself - * is provided by the platform runtime (and by `injectMocks` in tests); this - * interface mirrors it so callers can `import type { TailorDBFileError }` from - * the wrapper module without depending on any ambient declaration. - */ -export interface TailorDBFileError extends Error { - name: "TailorDBFileError"; - code?: TailorDBFileErrorCode; - cause?: unknown; -} - -// --- tailor.idp ----------------------------------------------------------- - -/** Configuration for creating an IDP Client. */ -export interface IdpClientConfig { - namespace: string; -} - -/** User object returned from IDP operations. */ -export interface IdpUser { - id: string; - name: string; - disabled: boolean; - createdAt?: string; - updatedAt?: string; -} - -/** Query options for filtering users. */ -export interface IdpUserQuery { - /** Filter by user IDs */ - ids?: string[]; - /** Filter by user names */ - names?: string[]; -} - -/** Options for listing users. */ -export interface IdpListUsersOptions { - /** Maximum number of users to return */ - first?: number; - /** Page token for pagination */ - after?: string; - /** Query filter for users */ - query?: IdpUserQuery; -} - -/** Response from listing users. */ -export interface IdpListUsersResponse { - users: IdpUser[]; - nextPageToken: string | null; - totalCount: number; -} - -/** Input for creating a new user. */ -export interface IdpCreateUserInput { - /** The user's name (typically email) */ - name: string; - /** The user's password. If omitted, the user is created without a password (cannot log in with any password). */ - password?: string; - /** Whether the user is disabled */ - disabled?: boolean; -} - -/** Input for updating an existing user. */ -export interface IdpUpdateUserInput { - /** The user's ID */ - id: string; - /** New name for the user */ - name?: string; - /** New password for the user. Cannot be used with clearPassword. */ - password?: string; - /** If true, remove the user's password. Cannot be used with password. */ - clearPassword?: boolean; - /** New disabled status for the user */ - disabled?: boolean; -} - -/** Input for sending a password reset email. */ -export interface IdpSendPasswordResetEmailInput { - /** The ID of the user */ - userId: string; - /** The URI to redirect to after password reset */ - redirectUri: string; - /** The sender display name. Defaults to 'Tailor Platform IdP'. */ - fromName?: string; - /** The email subject line. Defaults to the localized default subject. */ - subject?: string; -} - -/** Instance methods exposed by `tailor.idp.Client`. */ -export interface IdpClientInstance { - users(options?: IdpListUsersOptions): Promise; - user(userId: string): Promise; - userByName(name: string): Promise; - createUser(input: IdpCreateUserInput): Promise; - updateUser(input: IdpUpdateUserInput): Promise; - deleteUser(userId: string): Promise; - sendPasswordResetEmail(input: IdpSendPasswordResetEmailInput): Promise; -} - -/** Constructor shape for `tailor.idp.Client`. */ -export interface IdpClientConstructor { - new (config: IdpClientConfig): IdpClientInstance; -} - -// --- tailor.workflow ----------------------------------------------------- - -/** - * Specifies the machine user that should be used to execute the workflow. - * This allows workflows to run with specific authentication context. - */ -export interface WorkflowAuthInvoker { - /** The namespace where the machine user is defined */ - namespace: string; - /** The name of the machine user to use for workflow execution */ - machineUserName: string; -} - -/** Options for triggering a workflow. */ -export interface WorkflowTriggerWorkflowOptions { - /** Optional authentication invoker to specify which machine user should execute the workflow */ - authInvoker?: WorkflowAuthInvoker; -} - -// --- tailor.context ------------------------------------------------------- - -/** Information about the invoker of the current function execution. */ -export interface ContextInvoker { - /** The invoker's ID */ - id: string; - /** The invoker's type */ - type: "user" | "machine_user"; - /** The workspace ID */ - workspaceId: string; - /** The invoker's attribute IDs */ - attributes: string[]; - /** The invoker's attribute map */ - attributeMap: Record; -} - -// --- tailor.iconv --------------------------------------------------------- - -/** Instance methods exposed by `tailor.iconv.Iconv`. */ -export interface IconvInstance { - convert(input: string | Uint8Array | ArrayBuffer): string | Uint8Array; -} - -/** Constructor shape for `tailor.iconv.Iconv`. */ -export interface IconvConstructor { - new (fromEncoding: string, toEncoding: string): IconvInstance; -} - -// --------------------------------------------------------------------------- -// API surface types — describe the shape of `globalThis.tailor` / `tailordb` -// without polluting any ambient namespace -// --------------------------------------------------------------------------- - -/** `tailor.secretmanager` API surface. */ -export interface TailorSecretmanagerAPI { - getSecrets( - vault: string, - names: T, - ): Promise>>; - getSecret(vault: string, name: string): Promise; -} - -/** `tailor.authconnection` API surface. */ -export interface TailorAuthconnectionAPI { - getConnectionToken(connectionName: string): Promise; -} - -/** `tailor.iconv` API surface. */ -export interface TailorIconvAPI { - convert( - str: string | Uint8Array | ArrayBuffer, - fromEncoding: string, - toEncoding: T, - ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; - convertBuffer( - buffer: Uint8Array | ArrayBuffer, - fromEncoding: string, - toEncoding: T, - ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; - decode(buffer: Uint8Array | ArrayBuffer, encoding: string): string; - encode( - str: string, - encoding: T, - ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; - encodings(): string[]; - Iconv: IconvConstructor; -} - -/** `tailor.idp` API surface. */ -export interface TailorIdpAPI { - Client: IdpClientConstructor; -} - -/** `tailor.workflow` API surface. */ -export interface TailorWorkflowAPI { - triggerWorkflow( - workflow_name: string, - args?: any, - options?: WorkflowTriggerWorkflowOptions, - ): Promise; - triggerJobFunction(job_name: string, args?: any): any; - wait(key: string, payload?: any): any; - resolve(executionId: string, key: string, callback: (waitPayload: any) => any): Promise; -} - -/** `tailor.context` API surface. */ -export interface TailorContextAPI { - getInvoker(): ContextInvoker | null; -} - -/** Top-level `tailor` runtime object. */ -export interface TailorRuntime { - secretmanager: TailorSecretmanagerAPI; - authconnection: TailorAuthconnectionAPI; - iconv: TailorIconvAPI; - idp: TailorIdpAPI; - workflow: TailorWorkflowAPI; - context: TailorContextAPI; -} - -/** Instance methods exposed by `tailordb.Client`. */ -export interface TailordbClientInstance { - connect(): Promise; - end(): Promise; - queryObject(sql: string, args?: readonly unknown[]): Promise>; -} - -/** Constructor shape for `tailordb.Client`. */ -export interface TailordbClientConstructor { - new (config: { namespace: string }): TailordbClientInstance; -} - -/** Top-level `tailordb` runtime object. */ -export interface TailordbRuntime { - Client: TailordbClientConstructor; - file: TailorDBFileAPI; -} - -// --------------------------------------------------------------------------- -// Typed accessor — reads `tailor` / `tailordb` from globalThis lazily. -// Importing this value does NOT activate any ambient global declarations. -// --------------------------------------------------------------------------- - -interface RuntimeBindings { - readonly tailor: TailorRuntime; - readonly tailordb: TailordbRuntime; -} - -/** - * Lazy typed view of the platform runtime globals (`tailor`, `tailordb`). - * Each property read returns the current `globalThis` value, so test setups - * that inject mocks in `beforeEach` work without needing to re-import. - */ -export const runtime: RuntimeBindings = { - get tailor() { - return (globalThis as unknown as { tailor: TailorRuntime }).tailor; - }, - get tailordb() { - return (globalThis as unknown as { tailordb: TailordbRuntime }).tailordb; - }, -}; diff --git a/packages/sdk/src/runtime/authconnection.ts b/packages/sdk/src/runtime/authconnection.ts index 41c5430ac..cc79c19a7 100644 --- a/packages/sdk/src/runtime/authconnection.ts +++ b/packages/sdk/src/runtime/authconnection.ts @@ -10,7 +10,18 @@ * const token = await authconnection.getConnectionToken("my-connection"); */ -import { runtime } from "./_runtime"; +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { runtime } from "./internal"; + +/** + * Platform API surface for `tailor.authconnection`. Describes the shape the + * platform runtime injects on `globalThis.tailor.authconnection`. + * @internal + */ +export interface TailorAuthconnectionAPI { + getConnectionToken(connectionName: string): Promise; +} /** * Returns the access token for the given auth connection. diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index c103d397a..3bdc73aeb 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -13,7 +13,7 @@ * } */ -import { runtime } from "./_runtime"; +import { runtime } from "./internal"; /** * Information about the invoker of the current function execution. @@ -34,6 +34,33 @@ export interface Invoker { attributeList: string[]; } +/** + * Raw platform-side invoker payload returned by `tailor.context.getInvoker()`. + * The wrapper normalizes this into {@link Invoker}. + * @internal + */ +export interface ContextInvoker { + /** The invoker's ID */ + id: string; + /** The invoker's type */ + type: "user" | "machine_user"; + /** The workspace ID */ + workspaceId: string; + /** The invoker's attribute IDs */ + attributes: string[]; + /** The invoker's attribute map */ + attributeMap: Record; +} + +/** + * Platform API surface for `tailor.context`. Describes the shape the platform + * runtime injects on `globalThis.tailor.context`. + * @internal + */ +export interface TailorContextAPI { + getInvoker(): ContextInvoker | null; +} + /** * Returns information about the invoker of the current function execution, * or `null` for anonymous invocations. diff --git a/packages/sdk/src/runtime/file.ts b/packages/sdk/src/runtime/file.ts index 93a4f10ab..9f61d40b7 100644 --- a/packages/sdk/src/runtime/file.ts +++ b/packages/sdk/src/runtime/file.ts @@ -16,36 +16,140 @@ * ); */ -import { - runtime, - type DownloadMetadata, - type FileDownloadAsBase64Response, - type FileDownloadResponse, - type FileMetadata, - type FileStreamIterator, - type FileUploadOptions, - type FileUploadResponse, - type StreamMetadata, - type StreamValue, - type TailorDBFileError, - type TailorDBFileErrorCode, - type UploadMetadata, -} from "./_runtime"; - -export type { - UploadMetadata, - DownloadMetadata, - FileMetadata, - StreamMetadata, - FileUploadOptions, - FileUploadResponse, - FileDownloadResponse, - FileDownloadAsBase64Response, - StreamValue, - FileStreamIterator, - TailorDBFileError, - TailorDBFileErrorCode, -}; +import { runtime } from "./internal"; + +/** Upload response metadata. */ +export interface UploadMetadata { + fileSize: number; + sha256sum: string; +} + +/** Download response metadata. */ +export interface DownloadMetadata { + contentType: string; + fileSize: number; + sha256sum: string; + lastUploadedAt: string; +} + +/** File metadata (for {@link getMetadata}). */ +export interface FileMetadata { + contentType: string; + fileSize: number; + sha256sum: string; + urlPath: string; + lastUploadedAt?: string; +} + +/** Stream metadata (first chunk emitted by {@link openDownloadStream}). */ +export interface StreamMetadata { + contentType: string; + fileSize: number; + sha256sum: string; +} + +/** Upload options. */ +export interface FileUploadOptions { + contentType?: string; +} + +/** Upload response. */ +export interface FileUploadResponse { + metadata: UploadMetadata; +} + +/** Download response. */ +export interface FileDownloadResponse { + data: Uint8Array; + metadata: DownloadMetadata; +} + +/** Download-as-Base64 response. */ +export interface FileDownloadAsBase64Response { + data: string; + metadata: DownloadMetadata; +} + +/** Stream chunk types emitted by {@link FileStreamIterator}. */ +export type StreamValue = + | { type: "metadata"; metadata: StreamMetadata } + | { type: "chunk"; data: Uint8Array; position: number } + | { type: "complete" }; + +/** Stream iterator returned by {@link openDownloadStream}. */ +export interface FileStreamIterator extends AsyncIterableIterator { + next(): Promise>; + close(): Promise; +} + +/** Error code emitted by {@link TailorDBFileError}. */ +export type TailorDBFileErrorCode = + | "INVALID_PARAMS" + | "INVALID_DATA_TYPE" + | "OPERATION_FAILED" + | "DELETE_FAILED" + | "STREAM_OPEN_FAILED" + | "STREAM_READ_ERROR" + | "STREAM_ERROR" + | "FILE_TOO_LARGE"; + +/** + * Type-only shape of the `TailorDBFileError` runtime class. The class itself + * is provided by the platform runtime (and by `injectMocks` in tests); this + * interface mirrors it so callers can `import type { TailorDBFileError }` from + * the wrapper module without depending on any ambient declaration. + */ +export interface TailorDBFileError extends Error { + name: "TailorDBFileError"; + code?: TailorDBFileErrorCode; + cause?: unknown; +} + +/** + * Platform API surface for `tailordb.file`. Describes the shape the platform + * runtime injects on `globalThis.tailordb.file`. + * @internal + */ +export interface TailorDBFileAPI { + upload( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + data: string | ArrayBuffer | Uint8Array | number[], + options?: FileUploadOptions, + ): Promise; + + download( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + ): Promise; + + downloadAsBase64( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + ): Promise; + + delete(namespace: string, typeName: string, fieldName: string, recordId: string): Promise; + + getMetadata( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + ): Promise; + + openDownloadStream( + namespace: string, + typeName: string, + fieldName: string, + recordId: string, + ): Promise; +} /** * Upload a file to TailorDB. diff --git a/packages/sdk/src/runtime/globals.ts b/packages/sdk/src/runtime/globals.ts index 51387000c..80a584f06 100644 --- a/packages/sdk/src/runtime/globals.ts +++ b/packages/sdk/src/runtime/globals.ts @@ -18,23 +18,23 @@ /* eslint-disable @typescript-eslint/no-namespace, @typescript-eslint/no-explicit-any, jsdoc/require-param, jsdoc/require-returns, jsdoc/require-param-description */ +import type { ContextInvoker } from "./context"; +import type { TailorDBFileAPI, TailorDBFileErrorCode } from "./file"; import type { - ContextInvoker, - IdpClientConfig, - IdpCreateUserInput, - IdpListUsersOptions, - IdpListUsersResponse, - IdpSendPasswordResetEmailInput, - IdpUpdateUserInput, - IdpUser, - IdpUserQuery, - TailorDBFileAPI, - TailorDBFileErrorCode, - TailordbCommandType, - TailordbQueryResult, - WorkflowAuthInvoker, - WorkflowTriggerWorkflowOptions, -} from "./_runtime"; + ClientConfig as IdpClientConfig, + CreateUserInput as IdpCreateUserInput, + ListUsersOptions as IdpListUsersOptions, + ListUsersResponse as IdpListUsersResponse, + SendPasswordResetEmailInput as IdpSendPasswordResetEmailInput, + UpdateUserInput as IdpUpdateUserInput, + User as IdpUser, + UserQuery as IdpUserQuery, +} from "./idp"; +import type { TailordbCommandType, TailordbQueryResult } from "./internal"; +import type { + AuthInvoker as WorkflowAuthInvoker, + TriggerWorkflowOptions as WorkflowTriggerWorkflowOptions, +} from "./workflow"; declare global { namespace Tailordb { diff --git a/packages/sdk/src/runtime/iconv.ts b/packages/sdk/src/runtime/iconv.ts index e876099ea..cbbb89b42 100644 --- a/packages/sdk/src/runtime/iconv.ts +++ b/packages/sdk/src/runtime/iconv.ts @@ -15,7 +15,45 @@ * const out = conv.convert(sjisBuffer); */ -import { runtime, type IconvInstance } from "./_runtime"; +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { runtime } from "./internal"; + +/** Instance methods exposed by `tailor.iconv.Iconv`. */ +export interface IconvInstance { + convert(input: string | Uint8Array | ArrayBuffer): string | Uint8Array; +} + +/** Constructor shape for `tailor.iconv.Iconv`. */ +export interface IconvConstructor { + new (fromEncoding: string, toEncoding: string): IconvInstance; +} + +/** + * Platform API surface for `tailor.iconv`. Describes the shape the platform + * runtime injects on `globalThis.tailor.iconv` so the wrapper and ambient + * globals stay in sync. + * @internal + */ +export interface TailorIconvAPI { + convert( + str: string | Uint8Array | ArrayBuffer, + fromEncoding: string, + toEncoding: T, + ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; + convertBuffer( + buffer: Uint8Array | ArrayBuffer, + fromEncoding: string, + toEncoding: T, + ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; + decode(buffer: Uint8Array | ArrayBuffer, encoding: string): string; + encode( + str: string, + encoding: T, + ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; + encodings(): string[]; + Iconv: IconvConstructor; +} /** * Convert a string or buffer between encodings. diff --git a/packages/sdk/src/runtime/idp.ts b/packages/sdk/src/runtime/idp.ts index e76cd1494..eacd67c41 100644 --- a/packages/sdk/src/runtime/idp.ts +++ b/packages/sdk/src/runtime/idp.ts @@ -11,42 +11,113 @@ * const { users } = await client.users({ first: 10 }); */ -import { - runtime, - type IdpClientConfig, - type IdpClientInstance, - type IdpCreateUserInput, - type IdpListUsersOptions, - type IdpListUsersResponse, - type IdpSendPasswordResetEmailInput, - type IdpUpdateUserInput, - type IdpUser, - type IdpUserQuery, -} from "./_runtime"; +import { runtime } from "./internal"; /** Configuration object for {@link Client}. */ -export type ClientConfig = IdpClientConfig; +export interface ClientConfig { + namespace: string; +} /** User record returned by IDP operations. */ -export type User = IdpUser; +export interface User { + id: string; + name: string; + disabled: boolean; + createdAt?: string; + updatedAt?: string; +} /** Filter options for {@link Client.users}. */ -export type UserQuery = IdpUserQuery; +export interface UserQuery { + /** Filter by user IDs */ + ids?: string[]; + /** Filter by user names */ + names?: string[]; +} /** Pagination/filter options for {@link Client.users}. */ -export type ListUsersOptions = IdpListUsersOptions; +export interface ListUsersOptions { + /** Maximum number of users to return */ + first?: number; + /** Page token for pagination */ + after?: string; + /** Query filter for users */ + query?: UserQuery; +} /** Response shape for {@link Client.users}. */ -export type ListUsersResponse = IdpListUsersResponse; +export interface ListUsersResponse { + users: User[]; + nextPageToken: string | null; + totalCount: number; +} /** Input for {@link Client.createUser}. */ -export type CreateUserInput = IdpCreateUserInput; +export interface CreateUserInput { + /** The user's name (typically email) */ + name: string; + /** The user's password. If omitted, the user is created without a password (cannot log in with any password). */ + password?: string; + /** Whether the user is disabled */ + disabled?: boolean; +} /** Input for {@link Client.updateUser}. */ -export type UpdateUserInput = IdpUpdateUserInput; +export interface UpdateUserInput { + /** The user's ID */ + id: string; + /** New name for the user */ + name?: string; + /** New password for the user. Cannot be used with clearPassword. */ + password?: string; + /** If true, remove the user's password. Cannot be used with password. */ + clearPassword?: boolean; + /** New disabled status for the user */ + disabled?: boolean; +} /** Input for {@link Client.sendPasswordResetEmail}. */ -export type SendPasswordResetEmailInput = IdpSendPasswordResetEmailInput; +export interface SendPasswordResetEmailInput { + /** The ID of the user */ + userId: string; + /** The URI to redirect to after password reset */ + redirectUri: string; + /** The sender display name. Defaults to 'Tailor Platform IdP'. */ + fromName?: string; + /** The email subject line. Defaults to the localized default subject. */ + subject?: string; +} + +/** + * Instance methods exposed by `tailor.idp.Client`. + * @internal + */ +export interface IdpClientInstance { + users(options?: ListUsersOptions): Promise; + user(userId: string): Promise; + userByName(name: string): Promise; + createUser(input: CreateUserInput): Promise; + updateUser(input: UpdateUserInput): Promise; + deleteUser(userId: string): Promise; + sendPasswordResetEmail(input: SendPasswordResetEmailInput): Promise; +} + +/** + * Constructor shape for `tailor.idp.Client`. + * @internal + */ +export interface IdpClientConstructor { + new (config: ClientConfig): IdpClientInstance; +} + +/** + * Platform API surface for `tailor.idp`. Describes the shape the platform + * runtime injects on `globalThis.tailor.idp`. + * @internal + */ +export interface TailorIdpAPI { + Client: IdpClientConstructor; +} /** * IDP Client for user management operations. diff --git a/packages/sdk/src/runtime/internal.ts b/packages/sdk/src/runtime/internal.ts new file mode 100644 index 000000000..2232efe7d --- /dev/null +++ b/packages/sdk/src/runtime/internal.ts @@ -0,0 +1,103 @@ +/** + * Internal runtime bindings shared by the typed wrappers in + * `@tailor-platform/sdk/runtime/*`. Not part of the public API. + * + * - The exported `runtime` value reads `tailor` / `tailordb` from `globalThis` + * lazily through getters so wrappers stay decoupled from module-load order + * (mocks injected in `beforeEach` are picked up on next access). + * - The exported `TailorRuntime` / `TailordbRuntime` types aggregate the + * per-service API surfaces (which live alongside their wrappers in + * `./iconv`, `./idp`, etc.) into a single global shape. Importing this + * module does not introduce any ambient global declarations; the + * `declare global` block lives only in `./globals`. + * @internal + */ + +import type { TailorAuthconnectionAPI } from "./authconnection"; +import type { TailorContextAPI } from "./context"; +import type { TailorDBFileAPI } from "./file"; +import type { TailorIconvAPI } from "./iconv"; +import type { TailorIdpAPI } from "./idp"; +import type { TailorSecretmanagerAPI } from "./secretmanager"; +import type { TailorWorkflowAPI } from "./workflow"; + +// --------------------------------------------------------------------------- +// Tailordb client types — no service wrapper exists for the SQL Client, so +// these live here alongside the runtime accessor. +// --------------------------------------------------------------------------- + +/** SQL command type recorded on a {@link TailordbQueryResult}. */ +export type TailordbCommandType = + | "INSERT" + | "DELETE" + | "UPDATE" + | "SELECT" + | "MOVE" + | "FETCH" + | "COPY" + | "CREATE"; + +/** Result of a single `queryObject` call against the TailorDB driver. */ +export interface TailordbQueryResult { + rows: T[]; + command: TailordbCommandType; + rowCount: number; +} + +/** Instance methods exposed by `tailordb.Client`. */ +export interface TailordbClientInstance { + connect(): Promise; + end(): Promise; + queryObject(sql: string, args?: readonly unknown[]): Promise>; +} + +/** Constructor shape for `tailordb.Client`. */ +export interface TailordbClientConstructor { + new (config: { namespace: string }): TailordbClientInstance; +} + +// --------------------------------------------------------------------------- +// Top-level runtime shape — aggregates each service's API surface +// --------------------------------------------------------------------------- + +/** Top-level `tailor` runtime object. */ +export interface TailorRuntime { + secretmanager: TailorSecretmanagerAPI; + authconnection: TailorAuthconnectionAPI; + iconv: TailorIconvAPI; + idp: TailorIdpAPI; + workflow: TailorWorkflowAPI; + context: TailorContextAPI; +} + +/** Top-level `tailordb` runtime object. */ +export interface TailordbRuntime { + Client: TailordbClientConstructor; + file: TailorDBFileAPI; +} + +// --------------------------------------------------------------------------- +// Lazy typed accessor — reads `tailor` / `tailordb` from globalThis on every +// property access so test setups that swap globals in `beforeEach` are picked +// up without re-importing. Importing this value does NOT activate any ambient +// global declarations. +// --------------------------------------------------------------------------- + +interface RuntimeBindings { + readonly tailor: TailorRuntime; + readonly tailordb: TailordbRuntime; +} + +/** + * Lazy typed view of the platform runtime globals (`tailor`, `tailordb`). + * Each property read returns the current `globalThis` value, so test setups + * that inject mocks in `beforeEach` work without needing to re-import. + */ +export const runtime: RuntimeBindings = { + get tailor() { + return (globalThis as unknown as { tailor: TailorRuntime }).tailor; + }, + get tailordb() { + return (globalThis as unknown as { tailordb: TailordbRuntime }).tailordb; + }, +}; diff --git a/packages/sdk/src/runtime/secretmanager.ts b/packages/sdk/src/runtime/secretmanager.ts index 19faba3f5..a49f00430 100644 --- a/packages/sdk/src/runtime/secretmanager.ts +++ b/packages/sdk/src/runtime/secretmanager.ts @@ -12,7 +12,20 @@ * const all = await secretmanager.getSecrets("my-vault", ["A", "B"] as const); */ -import { runtime } from "./_runtime"; +import { runtime } from "./internal"; + +/** + * Platform API surface for `tailor.secretmanager`. Describes the shape the + * platform runtime injects on `globalThis.tailor.secretmanager`. + * @internal + */ +export interface TailorSecretmanagerAPI { + getSecrets( + vault: string, + names: T, + ): Promise>>; + getSecret(vault: string, name: string): Promise; +} /** * Returns multiple secrets from a vault. Missing names are omitted from the result. diff --git a/packages/sdk/src/runtime/workflow.ts b/packages/sdk/src/runtime/workflow.ts index 1803a9681..66d644b39 100644 --- a/packages/sdk/src/runtime/workflow.ts +++ b/packages/sdk/src/runtime/workflow.ts @@ -10,13 +10,42 @@ * const executionId = await workflow.triggerWorkflow("myWorkflow", { data: "value" }); */ -import { runtime, type WorkflowAuthInvoker, type WorkflowTriggerWorkflowOptions } from "./_runtime"; +/* eslint-disable @typescript-eslint/no-explicit-any */ -/** {@link triggerWorkflow} option type. */ -export type AuthInvoker = WorkflowAuthInvoker; +import { runtime } from "./internal"; -/** {@link triggerWorkflow} option bag. */ -export type TriggerWorkflowOptions = WorkflowTriggerWorkflowOptions; +/** + * Specifies the machine user that should be used to execute the workflow. + * This allows workflows to run with specific authentication context. + */ +export interface AuthInvoker { + /** The namespace where the machine user is defined */ + namespace: string; + /** The name of the machine user to use for workflow execution */ + machineUserName: string; +} + +/** Options for {@link triggerWorkflow}. */ +export interface TriggerWorkflowOptions { + /** Optional authentication invoker to specify which machine user should execute the workflow */ + authInvoker?: AuthInvoker; +} + +/** + * Platform API surface for `tailor.workflow`. Describes the shape the platform + * runtime injects on `globalThis.tailor.workflow`. + * @internal + */ +export interface TailorWorkflowAPI { + triggerWorkflow( + workflow_name: string, + args?: any, + options?: TriggerWorkflowOptions, + ): Promise; + triggerJobFunction(job_name: string, args?: any): any; + wait(key: string, payload?: any): any; + resolve(executionId: string, key: string, callback: (waitPayload: any) => any): Promise; +} /** * Triggers a workflow and returns its execution ID. diff --git a/packages/sdk/src/vitest/mock.ts b/packages/sdk/src/vitest/mock.ts index c999b73be..1a2ac23b8 100644 --- a/packages/sdk/src/vitest/mock.ts +++ b/packages/sdk/src/vitest/mock.ts @@ -6,8 +6,9 @@ * responses and assert on recorded calls via the exported mock objects. */ -import type { ContextInvoker, IdpUser, TailorDBFileErrorCode } from "../runtime/_runtime"; -import type { Invoker } from "../runtime/context"; +import type { ContextInvoker, Invoker } from "../runtime/context"; +import type { TailorDBFileErrorCode } from "../runtime/file"; +import type { User as IdpUser } from "../runtime/idp"; // --------------------------------------------------------------------------- // Types From 0749c012ebd59dfbeb067792f9dd173a31e42c73 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 14 May 2026 10:26:50 +0900 Subject: [PATCH 18/35] fix(sdk): activate ambient runtime globals via tsdown banner - Replace 'export * from @/runtime/globals' in configure/index.ts (which leaked an internal sentinel symbol) with a tsdown banner.dts triple-slash reference to '@tailor-platform/sdk/runtime/globals', mirroring how '@tailor-platform/function-types' was activated on main. - Strip the self-reference from runtime/globals.d.mts itself in an onSuccess hook to avoid TS1006 under skipLibCheck: false. - Drop contextMock from the vitest helpers. Consumers should test resolvers/executors at the body level (the 'invoker' arg) or, for bundled tests, via vi.spyOn(globalThis.tailor.context, 'getInvoker'). --- .changeset/runtime-wrapper.md | 1 - example/tests/bundled_execution.test.ts | 9 ++-- packages/sdk/docs/testing.md | 34 +----------- packages/sdk/src/configure/index.ts | 3 -- .../sdk/src/runtime/__tests__/context.test.ts | 12 ++--- packages/sdk/src/runtime/context.ts | 3 +- packages/sdk/src/runtime/globals.ts | 8 +-- packages/sdk/src/vitest/index.ts | 3 +- packages/sdk/src/vitest/mock.ts | 52 ++----------------- packages/sdk/tsdown.config.ts | 24 +++++++++ 10 files changed, 43 insertions(+), 106 deletions(-) diff --git a/.changeset/runtime-wrapper.md b/.changeset/runtime-wrapper.md index bcf23c510..5361ef8a4 100644 --- a/.changeset/runtime-wrapper.md +++ b/.changeset/runtime-wrapper.md @@ -17,5 +17,4 @@ The SDK no longer depends on the external `@tailor-platform/function-types` pack Other test-mock changes from `@tailor-platform/sdk/vitest`: -- New `contextMock` — control the invoker returned by `tailor.context.getInvoker()` and inspect call history. See the Testing Guide for usage. - Breaking: `fileMock.enqueueResult(...)` now rejects raw `Uint8Array` / `ArrayBuffer` payloads for `openDownloadStream`. Enqueue a structured iterable of `StreamValue` items (`{ type: "metadata" }`, `{ type: "chunk", data, position }`, `{ type: "complete" }`) so test streams stay aligned with the platform's structured stream contract. The shorthand `Uint8Array` enqueue is still accepted by `download` / `downloadAsBase64`. diff --git a/example/tests/bundled_execution.test.ts b/example/tests/bundled_execution.test.ts index 6f13a4daf..864b17239 100644 --- a/example/tests/bundled_execution.test.ts +++ b/example/tests/bundled_execution.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; -import { contextMock, tailordbMock, workflowMock } from "@tailor-platform/sdk/vitest"; +import { tailordbMock, workflowMock } from "@tailor-platform/sdk/vitest"; import { format as formatDate } from "date-fns"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; @@ -40,7 +40,6 @@ describe("bundled execution tests", () => { beforeEach(() => { tailordbMock.reset(); workflowMock.reset(); - contextMock.reset(); }); afterEach(() => { @@ -88,12 +87,12 @@ describe("bundled execution tests", () => { }); test("resolvers/showUserInfo.js returns user and invoker information", async () => { - contextMock.setInvoker({ + vi.spyOn(globalThis.tailor.context, "getInvoker").mockReturnValue({ id: "f1e2d3c4-b5a6-4798-89a0-1b2c3d4e5f60", type: "machine_user", workspaceId: "b39bdd61-d442-4a4e-8599-33a78a4e19ab", - attributes: { role: "MANAGER" }, - attributeList: [], + attributes: [], + attributeMap: { role: "MANAGER" }, }); const main = await importActualMain("resolvers/showUserInfo.js"); diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index 8c7c6dfc5..9fd62a3c9 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -24,7 +24,7 @@ Platform API mocks under `@tailor-platform/sdk/vitest` (auto-injected by the [`t - `tailordbMock` — TailorDB query stubs and call recording - `workflowMock` — `tailor.workflow` job / wait / resolve mocks -- `secretmanagerMock`, `authconnectionMock`, `idpMock`, `fileMock`, `iconvMock`, `contextMock` — corresponding platform API mocks +- `secretmanagerMock`, `authconnectionMock`, `idpMock`, `fileMock`, `iconvMock` — corresponding platform API mocks > The examples below call `tailor.*` / `tailordb.*` via the ambient globals. To make these snippets type-check in a fresh TypeScript project, either opt into the globals once (`import "@tailor-platform/sdk/runtime/globals"` in a setup file, or list it in `tsconfig.json`'s `compilerOptions.types`), or call the typed wrappers from `@tailor-platform/sdk/runtime/*` instead. @@ -270,38 +270,6 @@ test("mock encoding conversion", () => { }); ``` -### Context Mock - -Pass the invoker shape that matches `TailorUser` / `TailorActor`: `attributes` is the attribute map and `attributeList` is the array of attribute IDs. Pass `null` for an anonymous caller (the default). - -```typescript -import { contextMock } from "@tailor-platform/sdk/vitest"; -import { context } from "@tailor-platform/sdk/runtime"; - -beforeEach(() => contextMock.reset()); - -test("returns invoker information", () => { - contextMock.setInvoker({ - id: "f1e2d3c4-b5a6-4798-89a0-1b2c3d4e5f60", - type: "machine_user", - workspaceId: "b39bdd61-d442-4a4e-8599-33a78a4e19ab", - attributes: { role: "MANAGER" }, - attributeList: ["role"], - }); - - const invoker = context.getInvoker(); - expect(invoker?.type).toBe("machine_user"); - expect(invoker?.attributes).toEqual({ role: "MANAGER" }); - expect(contextMock.calls).toHaveLength(1); -}); - -test("anonymous caller", () => { - contextMock.setInvoker(null); // null is the default - - expect(context.getInvoker()).toBeNull(); -}); -``` - ### Loading Secrets from Config Pass a config path to load `defineSecretManager()` values into the mock: diff --git a/packages/sdk/src/configure/index.ts b/packages/sdk/src/configure/index.ts index 0a252d795..9147c5dcc 100644 --- a/packages/sdk/src/configure/index.ts +++ b/packages/sdk/src/configure/index.ts @@ -1,6 +1,3 @@ -// Activates the legacy `tailor.*` / `tailordb.*` ambient globals. Remove in v2.0. -export * from "@/runtime/globals"; - import { t as _t } from "@/configure/types"; import type * as helperTypes from "@/types/helpers"; diff --git a/packages/sdk/src/runtime/__tests__/context.test.ts b/packages/sdk/src/runtime/__tests__/context.test.ts index b45be9841..b9401421b 100644 --- a/packages/sdk/src/runtime/__tests__/context.test.ts +++ b/packages/sdk/src/runtime/__tests__/context.test.ts @@ -1,18 +1,18 @@ /** * Tests for `@tailor-platform/sdk/runtime/context` typed wrappers. */ -import { afterEach, beforeEach, describe, expect, expectTypeOf, test } from "vitest"; +import { afterEach, beforeEach, describe, expect, expectTypeOf, test, vi } from "vitest"; import * as context from "@/runtime/context"; -import { cleanupMocks, contextMock, injectMocks } from "@/vitest/mock"; +import { cleanupMocks, injectMocks } from "@/vitest/mock"; describe("@tailor-platform/sdk/runtime/context", () => { beforeEach(() => { injectMocks(globalThis); - contextMock.reset(); }); afterEach(() => { cleanupMocks(globalThis); + vi.restoreAllMocks(); }); test("getInvoker returns null for anonymous invocations", () => { @@ -23,12 +23,12 @@ describe("@tailor-platform/sdk/runtime/context", () => { }); test("getInvoker exposes SDK shape (attributes map + attributeList array)", () => { - contextMock.setInvoker({ + vi.spyOn(globalThis.tailor.context, "getInvoker").mockReturnValue({ id: "u-1", type: "machine_user", workspaceId: "ws-1", - attributes: { role: "MANAGER" }, - attributeList: ["role"], + attributes: ["role"], + attributeMap: { role: "MANAGER" }, }); const invoker = context.getInvoker(); diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index 3bdc73aeb..98a82541a 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -2,8 +2,7 @@ * Execution context utilities. * * Thin typed wrapper around the platform-provided `tailor.context` runtime API. - * At runtime this delegates to `globalThis.tailor.context`. Use `contextMock` - * from `@tailor-platform/sdk/vitest` to mock these calls in unit tests. + * At runtime this delegates to `globalThis.tailor.context`. * @example * import { context } from "@tailor-platform/sdk/runtime"; * diff --git a/packages/sdk/src/runtime/globals.ts b/packages/sdk/src/runtime/globals.ts index 80a584f06..9b3009643 100644 --- a/packages/sdk/src/runtime/globals.ts +++ b/packages/sdk/src/runtime/globals.ts @@ -285,10 +285,4 @@ declare global { } } -/** - * Sentinel marker so that bundlers retain this module's `declare global` block - * in the emitted `.d.mts` instead of tree-shaking it down to `export {}`. - * Not part of the public SDK API. - * @internal - */ -export const __TAILOR_RUNTIME_GLOBALS_LOADED__: true = true; +export {}; diff --git a/packages/sdk/src/vitest/index.ts b/packages/sdk/src/vitest/index.ts index d86a75d77..abc3479cb 100644 --- a/packages/sdk/src/vitest/index.ts +++ b/packages/sdk/src/vitest/index.ts @@ -22,7 +22,7 @@ import type { Plugin } from "vitest/config"; * * 3. **Platform API mocks** (environment) — All platform APIs are auto-injected with * control objects: `tailordbMock`, `workflowMock`, `secretmanagerMock`, - * `authconnectionMock`, `idpMock`, `fileMock`, `iconvMock`, `contextMock`. Each + * `authconnectionMock`, `idpMock`, `fileMock`, `iconvMock`. Each * provides response configuration, call recording, and reset. * * 4. **Environment resolution** — Rewrites `environment: "tailor-runtime"` to the @@ -67,5 +67,4 @@ export { idpMock, fileMock, iconvMock, - contextMock, } from "./mock"; diff --git a/packages/sdk/src/vitest/mock.ts b/packages/sdk/src/vitest/mock.ts index 1a2ac23b8..911653bf5 100644 --- a/packages/sdk/src/vitest/mock.ts +++ b/packages/sdk/src/vitest/mock.ts @@ -6,7 +6,7 @@ * responses and assert on recorded calls via the exported mock objects. */ -import type { ContextInvoker, Invoker } from "../runtime/context"; +import type { ContextInvoker } from "../runtime/context"; import type { TailorDBFileErrorCode } from "../runtime/file"; import type { User as IdpUser } from "../runtime/idp"; @@ -94,10 +94,6 @@ interface WorkflowCall { args: unknown[]; } -interface ContextCall { - method: "getInvoker"; -} - interface MockState { // TailorDB queryResolver: QueryResolver; @@ -129,9 +125,6 @@ interface MockState { // Iconv iconvResolver: IconvResolver | null; iconvCalls: IconvCall[]; - // Context - invoker: ContextInvoker | null; - contextCalls: ContextCall[]; } // --------------------------------------------------------------------------- @@ -180,8 +173,6 @@ function createDefaultState(): MockState { fileCalls: [], iconvResolver: null, iconvCalls: [], - invoker: null, - contextCalls: [], }; } @@ -418,40 +409,6 @@ export const workflowMock = { }, }; -// --------------------------------------------------------------------------- -// Context Mock -// --------------------------------------------------------------------------- - -/** Mock control for `tailor.context` — invoker store and call recording. */ -export const contextMock = { - /** - * Set the invoker returned by `context.getInvoker()`. Pass `null` to simulate - * an anonymous (unauthenticated) caller — the default. - * @param invoker - Invoker to return, or `null` for anonymous - */ - setInvoker(invoker: Invoker | null): void { - getState().invoker = invoker - ? { - id: invoker.id, - type: invoker.type, - workspaceId: invoker.workspaceId, - attributes: invoker.attributeList, - attributeMap: invoker.attributes, - } - : null; - }, - - get calls(): ContextCall[] { - return getState().contextCalls; - }, - - reset(): void { - const state = getState(); - state.invoker = null; - state.contextCalls.length = 0; - }, -}; - // --------------------------------------------------------------------------- // SecretManager Mock // --------------------------------------------------------------------------- @@ -725,10 +682,11 @@ async function mockResolve( // Mock: tailor.context // --------------------------------------------------------------------------- +// Stub-only injection. SDK consumers configure invokers at the body level +// (resolver/executor/workflow `.body()` `invoker` arg) or, for bundled tests, +// via `vi.spyOn(globalThis.tailor.context, "getInvoker")`. function mockGetInvoker(): ContextInvoker | null { - const state = getState(); - state.contextCalls.push({ method: "getInvoker" }); - return state.invoker; + return null; } // --------------------------------------------------------------------------- diff --git a/packages/sdk/tsdown.config.ts b/packages/sdk/tsdown.config.ts index 3e7b16e0d..1296c6835 100644 --- a/packages/sdk/tsdown.config.ts +++ b/packages/sdk/tsdown.config.ts @@ -1,7 +1,24 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; import Sonda from "sonda/rolldown"; import { defineConfig, type TsdownPluginOption } from "tsdown"; import { loadYamlText } from "./scripts/yaml-text-plugin.mjs"; +// `banner.dts` injects the triple-slash into every emitted d.mts, including +// `runtime/globals.d.mts` itself. Strip it from that one file to avoid a +// self-reference TS1006 when consumers typecheck with `skipLibCheck: false`. +function stripSelfReferenceFromGlobalsDts(outDir: string): void { + const target = path.resolve(outDir, "runtime/globals.d.mts"); + const content = readFileSync(target, "utf-8"); + const cleaned = content.replace( + /^\/\/\/ \n/, + "", + ); + if (cleaned !== content) { + writeFileSync(target, cleaned, "utf-8"); + } +} + function yamlText() { return { name: "yaml-text", @@ -65,7 +82,14 @@ export default defineConfig({ js: ".mjs", dts: ".d.mts", }), + // Remove in v2.0. + banner: { + dts: '/// ', + }, external: ["vite", "vitest"], // peer dependencies: prevent bundling, resolve at runtime sourcemap: true, plugins, + onSuccess: (config) => { + stripSelfReferenceFromGlobalsDts(config.outDir); + }, }); From 08abf150fc996991517c3f0d91043fceba3cacc1 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 14 May 2026 10:31:10 +0900 Subject: [PATCH 19/35] docs(sdk): drop function-types lockstep note from runtime guide --- packages/sdk/docs/runtime.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/sdk/docs/runtime.md b/packages/sdk/docs/runtime.md index a94ee18c2..85024a565 100644 --- a/packages/sdk/docs/runtime.md +++ b/packages/sdk/docs/runtime.md @@ -46,8 +46,6 @@ import * as iconv from "@tailor-platform/sdk/runtime/iconv"; import type { ListUsersResponse, ClientConfig } from "@tailor-platform/sdk/runtime/idp"; ``` -> Type-only re-exports follow the platform contract. If a future runtime release adds new fields, the SDK will publish them in lockstep — there is no separate `@tailor-platform/function-types` package to upgrade. - ## Activating the global types Most users do not need to touch the globals entry — `@tailor-platform/sdk/runtime` (and its subpath modules) cover the same surface without depending on any ambient declaration. From ed326a0f621077b060e7a61c0b3e91b2d1d00856 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 14 May 2026 10:32:31 +0900 Subject: [PATCH 20/35] docs(sdk): drop globals opt-in note from testing guide --- packages/sdk/docs/testing.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/sdk/docs/testing.md b/packages/sdk/docs/testing.md index 9fd62a3c9..647175fbb 100644 --- a/packages/sdk/docs/testing.md +++ b/packages/sdk/docs/testing.md @@ -26,8 +26,6 @@ Platform API mocks under `@tailor-platform/sdk/vitest` (auto-injected by the [`t - `workflowMock` — `tailor.workflow` job / wait / resolve mocks - `secretmanagerMock`, `authconnectionMock`, `idpMock`, `fileMock`, `iconvMock` — corresponding platform API mocks -> The examples below call `tailor.*` / `tailordb.*` via the ambient globals. To make these snippets type-check in a fresh TypeScript project, either opt into the globals once (`import "@tailor-platform/sdk/runtime/globals"` in a setup file, or list it in `tsconfig.json`'s `compilerOptions.types`), or call the typed wrappers from `@tailor-platform/sdk/runtime/*` instead. - For tighter alignment with the production runtime — Node.js module blocking, Web-only globals, and platform API mocks — pair the resolver helpers with the [`tailor-runtime` Vitest environment](#runtime-environment-emulation-beta) below. Three starter templates demonstrate the patterns below in a working project: From 5349265a4cf8ceba3423ea05d9641ac6f0eead8c Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 14 May 2026 11:31:14 +0900 Subject: [PATCH 21/35] fix(sdk): scope ambient globals banner to the configure entry only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Banner is now stripped from every emitted .d.mts except configure/index.d.mts (the @tailor-platform/sdk main entry). Subpath imports like @tailor-platform/sdk/runtime, /vitest, /plugin, /cli stay self-contained — no implicit ambient globals activation. --- packages/sdk/src/runtime/index.ts | 8 +------ packages/sdk/tsdown.config.ts | 38 +++++++++++++++++++------------ 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/packages/sdk/src/runtime/index.ts b/packages/sdk/src/runtime/index.ts index 0c2c44408..743d7b3a2 100644 --- a/packages/sdk/src/runtime/index.ts +++ b/packages/sdk/src/runtime/index.ts @@ -2,19 +2,13 @@ * Typed wrappers for the Tailor Platform Function runtime APIs. * * Each namespace mirrors the corresponding `tailor.*` (or `tailordb.file`) - * surface that the platform runtime exposes globally, so consumers can write: + * surface that the platform runtime exposes globally. * @example * import { iconv, secretmanager, idp, workflow, file } from "@tailor-platform/sdk/runtime"; * * const utf8 = iconv.convert(sjisBuffer, "Shift_JIS", "UTF-8"); * const secret = await secretmanager.getSecret("my-vault", "API_KEY"); * const client = new idp.Client({ namespace: "my-namespace" }); - * - * Importing this entry does NOT activate the ambient `tailor.*` / `tailordb` - * global types — the wrappers and their associated types are self-contained. - * If you want to call `tailor.iconv.convert(...)` directly, add a side-effect - * import of `@tailor-platform/sdk/runtime/globals` (or list it in tsconfig - * `compilerOptions.types`). */ export * as iconv from "./iconv"; diff --git a/packages/sdk/tsdown.config.ts b/packages/sdk/tsdown.config.ts index 1296c6835..242d41cac 100644 --- a/packages/sdk/tsdown.config.ts +++ b/packages/sdk/tsdown.config.ts @@ -1,22 +1,30 @@ -import { readFileSync, writeFileSync } from "node:fs"; +import { readdirSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; import Sonda from "sonda/rolldown"; import { defineConfig, type TsdownPluginOption } from "tsdown"; import { loadYamlText } from "./scripts/yaml-text-plugin.mjs"; -// `banner.dts` injects the triple-slash into every emitted d.mts, including -// `runtime/globals.d.mts` itself. Strip it from that one file to avoid a -// self-reference TS1006 when consumers typecheck with `skipLibCheck: false`. -function stripSelfReferenceFromGlobalsDts(outDir: string): void { - const target = path.resolve(outDir, "runtime/globals.d.mts"); - const content = readFileSync(target, "utf-8"); - const cleaned = content.replace( - /^\/\/\/ \n/, - "", - ); - if (cleaned !== content) { - writeFileSync(target, cleaned, "utf-8"); - } +// `banner.dts` injects the triple-slash into every emitted d.mts. Keep it only +// on `configure/index.d.mts` (the `@tailor-platform/sdk` main entry) so that +// the legacy ambient globals stay active for that import path through v2.0. +// Strip it from every other `.d.mts` so subpath imports +// (`@tailor-platform/sdk/runtime`, `/vitest`, /plugin`, etc.) stay self-contained. +function stripBannerExceptConfigureEntry(outDir: string): void { + const pattern = /^\/\/\/ \n/; + const keep = path.resolve(outDir, "configure/index.d.mts"); + const walk = (dir: string): void => { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + walk(full); + } else if (entry.isFile() && entry.name.endsWith(".d.mts") && full !== keep) { + const content = readFileSync(full, "utf-8"); + const cleaned = content.replace(pattern, ""); + if (cleaned !== content) writeFileSync(full, cleaned, "utf-8"); + } + } + }; + walk(outDir); } function yamlText() { @@ -90,6 +98,6 @@ export default defineConfig({ sourcemap: true, plugins, onSuccess: (config) => { - stripSelfReferenceFromGlobalsDts(config.outDir); + stripBannerExceptConfigureEntry(config.outDir); }, }); From d1a1215b40d23f74402ef172dc27441eab991e04 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Thu, 14 May 2026 13:21:56 +0900 Subject: [PATCH 22/35] fix(sdk): correct configure-entry banner scoping and download-mock note --- .changeset/runtime-wrapper.md | 2 +- packages/sdk/tsdown.config.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.changeset/runtime-wrapper.md b/.changeset/runtime-wrapper.md index 5361ef8a4..d172ca63a 100644 --- a/.changeset/runtime-wrapper.md +++ b/.changeset/runtime-wrapper.md @@ -17,4 +17,4 @@ The SDK no longer depends on the external `@tailor-platform/function-types` pack Other test-mock changes from `@tailor-platform/sdk/vitest`: -- Breaking: `fileMock.enqueueResult(...)` now rejects raw `Uint8Array` / `ArrayBuffer` payloads for `openDownloadStream`. Enqueue a structured iterable of `StreamValue` items (`{ type: "metadata" }`, `{ type: "chunk", data, position }`, `{ type: "complete" }`) so test streams stay aligned with the platform's structured stream contract. The shorthand `Uint8Array` enqueue is still accepted by `download` / `downloadAsBase64`. +- Breaking: when an `openDownloadStream` (or `toFileStream()`) call consumes a queued mock result, raw `Uint8Array` / `ArrayBuffer` payloads are now rejected. Enqueue a structured iterable of `StreamValue` items (`{ type: "metadata" }`, `{ type: "chunk", data, position }`, `{ type: "complete" }`) so test streams stay aligned with the platform's structured stream contract. The shorthand `Uint8Array` enqueue is still accepted by `download` / `downloadAsBase64`. diff --git a/packages/sdk/tsdown.config.ts b/packages/sdk/tsdown.config.ts index 242d41cac..5351123da 100644 --- a/packages/sdk/tsdown.config.ts +++ b/packages/sdk/tsdown.config.ts @@ -11,7 +11,8 @@ import { loadYamlText } from "./scripts/yaml-text-plugin.mjs"; // (`@tailor-platform/sdk/runtime`, `/vitest`, /plugin`, etc.) stay self-contained. function stripBannerExceptConfigureEntry(outDir: string): void { const pattern = /^\/\/\/ \n/; - const keep = path.resolve(outDir, "configure/index.d.mts"); + const root = path.resolve(outDir); + const keep = path.join(root, "configure", "index.d.mts"); const walk = (dir: string): void => { for (const entry of readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); @@ -24,7 +25,7 @@ function stripBannerExceptConfigureEntry(outDir: string): void { } } }; - walk(outDir); + walk(root); } function yamlText() { From cdbd2600ab573d0402065d53d6244d31fbdb5283 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Mon, 18 May 2026 21:53:53 +0900 Subject: [PATCH 23/35] refactor(sdk): address PR review feedback - Rename TailorWorkflowAPI params (workflow_name/job_name) to camelCase - Reword TailorDBFileError test to reflect structural compatibility - Trim docs/runtime.md API Reference; let JSDoc serve as the SSOT - Colocate runtime/ and vitest/ tests next to their sources (no __tests__/) - Update knip ignore list to cover dynamically referenced fixtures and the nested integration vitest.config now that the __tests__ include pattern is gone from vitest.config.ts --- packages/sdk/docs/runtime.md | 74 +++---------------- packages/sdk/knip.json | 2 + .../{__tests__ => }/authconnection.test.ts | 0 .../runtime/{__tests__ => }/context.test.ts | 0 .../src/runtime/{__tests__ => }/file.test.ts | 5 +- .../runtime/{__tests__ => }/globals.test.ts | 0 .../src/runtime/{__tests__ => }/iconv.test.ts | 0 .../src/runtime/{__tests__ => }/idp.test.ts | 0 .../{__tests__ => }/secretmanager.test.ts | 0 .../runtime/{__tests__ => }/workflow.test.ts | 0 packages/sdk/src/runtime/workflow.ts | 16 ++-- .../{__tests__ => }/blocked-modules.test.ts | 2 +- .../src/vitest/{__tests__ => }/index.test.ts | 2 +- .../{__tests__ => }/integration.test.ts | 2 +- .../fixtures/uses-node-crypto-types.ts | 0 .../integration/fixtures/uses-node-crypto.ts | 0 .../integration/fixtures/uses-web-crypto.ts | 0 .../integration/should-fail.test.ts | 0 .../integration/should-pass.test.ts | 0 .../integration/vitest.config.ts | 6 +- .../vitest/{__tests__ => }/mock-types.test.ts | 2 +- .../src/vitest/{__tests__ => }/mock.test.ts | 2 +- .../src/vitest/{__tests__ => }/plugin.test.ts | 2 +- .../src/vitest/{__tests__ => }/setup.test.ts | 2 +- packages/sdk/vitest.config.ts | 5 +- 25 files changed, 34 insertions(+), 88 deletions(-) rename packages/sdk/src/runtime/{__tests__ => }/authconnection.test.ts (100%) rename packages/sdk/src/runtime/{__tests__ => }/context.test.ts (100%) rename packages/sdk/src/runtime/{__tests__ => }/file.test.ts (94%) rename packages/sdk/src/runtime/{__tests__ => }/globals.test.ts (100%) rename packages/sdk/src/runtime/{__tests__ => }/iconv.test.ts (100%) rename packages/sdk/src/runtime/{__tests__ => }/idp.test.ts (100%) rename packages/sdk/src/runtime/{__tests__ => }/secretmanager.test.ts (100%) rename packages/sdk/src/runtime/{__tests__ => }/workflow.test.ts (100%) rename packages/sdk/src/vitest/{__tests__ => }/blocked-modules.test.ts (97%) rename packages/sdk/src/vitest/{__tests__ => }/index.test.ts (97%) rename packages/sdk/src/vitest/{__tests__ => }/integration.test.ts (98%) rename packages/sdk/src/vitest/{__tests__ => }/integration/fixtures/uses-node-crypto-types.ts (100%) rename packages/sdk/src/vitest/{__tests__ => }/integration/fixtures/uses-node-crypto.ts (100%) rename packages/sdk/src/vitest/{__tests__ => }/integration/fixtures/uses-web-crypto.ts (100%) rename packages/sdk/src/vitest/{__tests__ => }/integration/should-fail.test.ts (100%) rename packages/sdk/src/vitest/{__tests__ => }/integration/should-pass.test.ts (100%) rename packages/sdk/src/vitest/{__tests__ => }/integration/vitest.config.ts (68%) rename packages/sdk/src/vitest/{__tests__ => }/mock-types.test.ts (96%) rename packages/sdk/src/vitest/{__tests__ => }/mock.test.ts (99%) rename packages/sdk/src/vitest/{__tests__ => }/plugin.test.ts (99%) rename packages/sdk/src/vitest/{__tests__ => }/setup.test.ts (99%) diff --git a/packages/sdk/docs/runtime.md b/packages/sdk/docs/runtime.md index 85024a565..0d3e31679 100644 --- a/packages/sdk/docs/runtime.md +++ b/packages/sdk/docs/runtime.md @@ -68,73 +68,17 @@ Or register the entry in `tsconfig.json`: } ``` -## API Reference +## Namespaces -### `iconv` +The runtime entry re-exports the following namespaces. Detailed signatures, parameters, and return types live in the JSDoc next to each export — hover the symbol in your IDE or browse the source. -Character encoding conversion. The return type narrows to `string` for `"UTF8"` / `"UTF-8"` targets and `Uint8Array` otherwise. - -| Function | Description | -| --------------- | ---------------------------------------------------------- | -| `convert` | Convert a string or buffer between encodings | -| `convertBuffer` | Convert bytes between encodings | -| `decode` | Decode bytes to a UTF-8 string | -| `encode` | Encode a UTF-8 string into the given target encoding | -| `encodings` | List supported encoding names | -| `Iconv` (class) | Stateful converter for repeated conversions (`node-iconv`) | - -### `secretmanager` - -| Function | Returns | -| ------------ | --------------------------------------------- | -| `getSecret` | `Promise` | -| `getSecrets` | `Promise>>` | - -Pass the `names` argument as a `const` tuple to narrow the result keys: `getSecrets("v", ["A", "B"] as const)`. - -### `authconnection` - -| Function | Returns | -| -------------------- | ----------------------- | -| `getConnectionToken` | Provider-specific token | - -### `idp` - -`new idp.Client({ namespace })` exposes the IdP user APIs: - -- `users(options?)`, `user(userId)`, `userByName(name)` -- `createUser(input)`, `updateUser(input)`, `deleteUser(userId)` -- `sendPasswordResetEmail({ userId, redirectUri })` - -### `workflow` - -| Function | Description | -| -------------------- | ---------------------------------------------- | -| `triggerWorkflow` | Trigger a workflow and return its execution ID | -| `triggerJobFunction` | Trigger a job and return its result | -| `wait` | Suspend a job at a wait point | -| `resolve` | Resolve a wait point on a running execution | - -### `context` - -| Function | Returns | -| ------------ | -------------------------------------- | -| `getInvoker` | `Invoker \| null` (anonymous = `null`) | - -### `file` - -`tailordb.file` BLOB API. - -| Function | Description | -| -------------------- | -------------------------------------------------- | -| `upload` | Upload bytes for a record's file field | -| `download` | Download a file (≤ 10 MB) | -| `downloadAsBase64` | Download a file as a Base64 string (≤ 10 MB) | -| `delete` | Delete a file | -| `getMetadata` | Fetch file metadata only | -| `openDownloadStream` | Open an async iterator for files larger than 10 MB | - -For files larger than 10 MB, `download` and `downloadAsBase64` throw `TailorDBFileError` with code `FILE_TOO_LARGE`; switch to `openDownloadStream` for those. +- `iconv` — character encoding conversion (`convert`, `convertBuffer`, `decode`, `encode`, `encodings`, `Iconv`) +- `secretmanager` — secret-vault access (`getSecret`, `getSecrets`) +- `authconnection` — OAuth-style connection tokens (`getConnectionToken`) +- `idp` — IdP user management (`new Client({ namespace })`) +- `workflow` — workflow & job control (`triggerWorkflow`, `triggerJobFunction`, `wait`, `resolve`) +- `context` — execution context (`getInvoker`) +- `file` — `tailordb.file` BLOB API (`upload`, `download`, `downloadAsBase64`, `delete`, `getMetadata`, `openDownloadStream`) ## Testing diff --git a/packages/sdk/knip.json b/packages/sdk/knip.json index 5d54abc18..a89433445 100644 --- a/packages/sdk/knip.json +++ b/packages/sdk/knip.json @@ -5,8 +5,10 @@ "ignore": [ "scripts/**", "e2e/fixtures/**", + "eslint-rules/__tests__/fixtures/**", "src/cli/commands/deploy/__test_fixtures__/**", "src/types/*.ts", + "src/vitest/integration/vitest.config.ts", "zinfer.config.ts" ], "ignoreDependencies": [ diff --git a/packages/sdk/src/runtime/__tests__/authconnection.test.ts b/packages/sdk/src/runtime/authconnection.test.ts similarity index 100% rename from packages/sdk/src/runtime/__tests__/authconnection.test.ts rename to packages/sdk/src/runtime/authconnection.test.ts diff --git a/packages/sdk/src/runtime/__tests__/context.test.ts b/packages/sdk/src/runtime/context.test.ts similarity index 100% rename from packages/sdk/src/runtime/__tests__/context.test.ts rename to packages/sdk/src/runtime/context.test.ts diff --git a/packages/sdk/src/runtime/__tests__/file.test.ts b/packages/sdk/src/runtime/file.test.ts similarity index 94% rename from packages/sdk/src/runtime/__tests__/file.test.ts rename to packages/sdk/src/runtime/file.test.ts index bdc66324d..3ec7acf52 100644 --- a/packages/sdk/src/runtime/__tests__/file.test.ts +++ b/packages/sdk/src/runtime/file.test.ts @@ -117,7 +117,7 @@ describe("@tailor-platform/sdk/runtime/file", () => { expect(fileMock.calls[0]?.method).toBe("openDownloadStream"); }); - test("TailorDBFileError type alias resolves to globalThis class", () => { + test("TailorDBFileError structurally matches globalThis class", () => { const TailorDBFileError = ( globalThis as unknown as { TailorDBFileError: new ( @@ -129,7 +129,8 @@ describe("@tailor-platform/sdk/runtime/file", () => { const err = new TailorDBFileError("operation failed", "OPERATION_FAILED"); expect(err.name).toBe("TailorDBFileError"); expect(err.code).toBe("OPERATION_FAILED"); - // Type-level: file.TailorDBFileError is the global class + // Type-level: file.TailorDBFileError is a structural interface that the + // global class instances satisfy (not a direct alias of the class itself). const _typed: file.TailorDBFileError = err as file.TailorDBFileError; expect(_typed).toBe(err); }); diff --git a/packages/sdk/src/runtime/__tests__/globals.test.ts b/packages/sdk/src/runtime/globals.test.ts similarity index 100% rename from packages/sdk/src/runtime/__tests__/globals.test.ts rename to packages/sdk/src/runtime/globals.test.ts diff --git a/packages/sdk/src/runtime/__tests__/iconv.test.ts b/packages/sdk/src/runtime/iconv.test.ts similarity index 100% rename from packages/sdk/src/runtime/__tests__/iconv.test.ts rename to packages/sdk/src/runtime/iconv.test.ts diff --git a/packages/sdk/src/runtime/__tests__/idp.test.ts b/packages/sdk/src/runtime/idp.test.ts similarity index 100% rename from packages/sdk/src/runtime/__tests__/idp.test.ts rename to packages/sdk/src/runtime/idp.test.ts diff --git a/packages/sdk/src/runtime/__tests__/secretmanager.test.ts b/packages/sdk/src/runtime/secretmanager.test.ts similarity index 100% rename from packages/sdk/src/runtime/__tests__/secretmanager.test.ts rename to packages/sdk/src/runtime/secretmanager.test.ts diff --git a/packages/sdk/src/runtime/__tests__/workflow.test.ts b/packages/sdk/src/runtime/workflow.test.ts similarity index 100% rename from packages/sdk/src/runtime/__tests__/workflow.test.ts rename to packages/sdk/src/runtime/workflow.test.ts diff --git a/packages/sdk/src/runtime/workflow.ts b/packages/sdk/src/runtime/workflow.ts index 66d644b39..9db657075 100644 --- a/packages/sdk/src/runtime/workflow.ts +++ b/packages/sdk/src/runtime/workflow.ts @@ -38,40 +38,40 @@ export interface TriggerWorkflowOptions { */ export interface TailorWorkflowAPI { triggerWorkflow( - workflow_name: string, + workflowName: string, args?: any, options?: TriggerWorkflowOptions, ): Promise; - triggerJobFunction(job_name: string, args?: any): any; + triggerJobFunction(jobName: string, args?: any): any; wait(key: string, payload?: any): any; resolve(executionId: string, key: string, callback: (waitPayload: any) => any): Promise; } /** * Triggers a workflow and returns its execution ID. - * @param workflow_name - Workflow name as defined in tailor.config + * @param workflowName - Workflow name as defined in tailor.config * @param args - Arguments forwarded to the workflow's main job * @param options - Optional trigger options (e.g. `authInvoker`) * @returns The execution ID of the triggered workflow */ export function triggerWorkflow( - workflow_name: string, + workflowName: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any args?: any, options?: TriggerWorkflowOptions, ): Promise { - return runtime.tailor.workflow.triggerWorkflow(workflow_name, args, options); + return runtime.tailor.workflow.triggerWorkflow(workflowName, args, options); } /** * Triggers a job function and returns its result. - * @param job_name - Job name as defined in the workflow + * @param jobName - Job name as defined in the workflow * @param args - Arguments forwarded to the job * @returns The job's return value */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function triggerJobFunction(job_name: string, args?: any): any { - return runtime.tailor.workflow.triggerJobFunction(job_name, args); +export function triggerJobFunction(jobName: string, args?: any): any { + return runtime.tailor.workflow.triggerJobFunction(jobName, args); } /** diff --git a/packages/sdk/src/vitest/__tests__/blocked-modules.test.ts b/packages/sdk/src/vitest/blocked-modules.test.ts similarity index 97% rename from packages/sdk/src/vitest/__tests__/blocked-modules.test.ts rename to packages/sdk/src/vitest/blocked-modules.test.ts index 7bb3ea406..622ca108d 100644 --- a/packages/sdk/src/vitest/__tests__/blocked-modules.test.ts +++ b/packages/sdk/src/vitest/blocked-modules.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { getBlockedMessage, isBlockedModule } from "../blocked-modules"; +import { getBlockedMessage, isBlockedModule } from "./blocked-modules"; describe("isBlockedModule", () => { test("recognizes node:-prefixed builtins", () => { diff --git a/packages/sdk/src/vitest/__tests__/index.test.ts b/packages/sdk/src/vitest/index.test.ts similarity index 97% rename from packages/sdk/src/vitest/__tests__/index.test.ts rename to packages/sdk/src/vitest/index.test.ts index 6330780b4..496970ac9 100644 --- a/packages/sdk/src/vitest/__tests__/index.test.ts +++ b/packages/sdk/src/vitest/index.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { isAbsolute } from "node:path"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import { tailorRuntime } from "../index"; +import { tailorRuntime } from "./index"; describe("tailorRuntime", () => { const ENV_VAR = "__TAILOR_RUNTIME_CONFIG"; diff --git a/packages/sdk/src/vitest/__tests__/integration.test.ts b/packages/sdk/src/vitest/integration.test.ts similarity index 98% rename from packages/sdk/src/vitest/__tests__/integration.test.ts rename to packages/sdk/src/vitest/integration.test.ts index 21657be7a..c24651495 100644 --- a/packages/sdk/src/vitest/__tests__/integration.test.ts +++ b/packages/sdk/src/vitest/integration.test.ts @@ -12,7 +12,7 @@ const configPath = resolve(integrationDir, "vitest.config.ts"); // Run the nested `vitest run` from the SDK package root (not src/) so the // subprocess sees the package's `package.json` and `tsconfig.json` for // module resolution and TS transforms. -const sdkDir = resolve(currentDir, "../../.."); +const sdkDir = resolve(currentDir, "../.."); // Resolve the workspace's installed Vitest entry rather than relying on `npx`, // which may perform online package resolution and slow down / destabilize CI. diff --git a/packages/sdk/src/vitest/__tests__/integration/fixtures/uses-node-crypto-types.ts b/packages/sdk/src/vitest/integration/fixtures/uses-node-crypto-types.ts similarity index 100% rename from packages/sdk/src/vitest/__tests__/integration/fixtures/uses-node-crypto-types.ts rename to packages/sdk/src/vitest/integration/fixtures/uses-node-crypto-types.ts diff --git a/packages/sdk/src/vitest/__tests__/integration/fixtures/uses-node-crypto.ts b/packages/sdk/src/vitest/integration/fixtures/uses-node-crypto.ts similarity index 100% rename from packages/sdk/src/vitest/__tests__/integration/fixtures/uses-node-crypto.ts rename to packages/sdk/src/vitest/integration/fixtures/uses-node-crypto.ts diff --git a/packages/sdk/src/vitest/__tests__/integration/fixtures/uses-web-crypto.ts b/packages/sdk/src/vitest/integration/fixtures/uses-web-crypto.ts similarity index 100% rename from packages/sdk/src/vitest/__tests__/integration/fixtures/uses-web-crypto.ts rename to packages/sdk/src/vitest/integration/fixtures/uses-web-crypto.ts diff --git a/packages/sdk/src/vitest/__tests__/integration/should-fail.test.ts b/packages/sdk/src/vitest/integration/should-fail.test.ts similarity index 100% rename from packages/sdk/src/vitest/__tests__/integration/should-fail.test.ts rename to packages/sdk/src/vitest/integration/should-fail.test.ts diff --git a/packages/sdk/src/vitest/__tests__/integration/should-pass.test.ts b/packages/sdk/src/vitest/integration/should-pass.test.ts similarity index 100% rename from packages/sdk/src/vitest/__tests__/integration/should-pass.test.ts rename to packages/sdk/src/vitest/integration/should-pass.test.ts diff --git a/packages/sdk/src/vitest/__tests__/integration/vitest.config.ts b/packages/sdk/src/vitest/integration/vitest.config.ts similarity index 68% rename from packages/sdk/src/vitest/__tests__/integration/vitest.config.ts rename to packages/sdk/src/vitest/integration/vitest.config.ts index 364eb0ff3..0168b6222 100644 --- a/packages/sdk/src/vitest/__tests__/integration/vitest.config.ts +++ b/packages/sdk/src/vitest/integration/vitest.config.ts @@ -1,7 +1,7 @@ import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; -import { createBlockPlugin } from "../../plugin"; +import { createBlockPlugin } from "../plugin"; const here = dirname(fileURLToPath(import.meta.url)); @@ -9,8 +9,8 @@ export default defineConfig({ plugins: [createBlockPlugin()], test: { watch: false, - environment: resolve(here, "../../environment.ts"), - setupFiles: [resolve(here, "../../setup.ts")], + environment: resolve(here, "../environment.ts"), + setupFiles: [resolve(here, "../setup.ts")], include: ["./**/*.test.ts"], root: here, }, diff --git a/packages/sdk/src/vitest/__tests__/mock-types.test.ts b/packages/sdk/src/vitest/mock-types.test.ts similarity index 96% rename from packages/sdk/src/vitest/__tests__/mock-types.test.ts rename to packages/sdk/src/vitest/mock-types.test.ts index 05f982bab..ce6c1778b 100644 --- a/packages/sdk/src/vitest/__tests__/mock-types.test.ts +++ b/packages/sdk/src/vitest/mock-types.test.ts @@ -8,7 +8,7 @@ */ import "@/runtime/globals"; import { afterAll, beforeAll, describe, expectTypeOf, test } from "vitest"; -import { injectMocks, cleanupMocks } from "../mock"; +import { injectMocks, cleanupMocks } from "./mock"; beforeAll(() => injectMocks(globalThis)); afterAll(() => cleanupMocks(globalThis)); diff --git a/packages/sdk/src/vitest/__tests__/mock.test.ts b/packages/sdk/src/vitest/mock.test.ts similarity index 99% rename from packages/sdk/src/vitest/__tests__/mock.test.ts rename to packages/sdk/src/vitest/mock.test.ts index 8af410579..b53b0c943 100644 --- a/packages/sdk/src/vitest/__tests__/mock.test.ts +++ b/packages/sdk/src/vitest/mock.test.ts @@ -12,7 +12,7 @@ import { cleanupMocks, STATE_KEY, RUNTIME_FLAG_KEY, -} from "../mock"; +} from "./mock"; describe("mock", () => { beforeEach(() => { diff --git a/packages/sdk/src/vitest/__tests__/plugin.test.ts b/packages/sdk/src/vitest/plugin.test.ts similarity index 99% rename from packages/sdk/src/vitest/__tests__/plugin.test.ts rename to packages/sdk/src/vitest/plugin.test.ts index 1ff0cbe57..fc67a4699 100644 --- a/packages/sdk/src/vitest/__tests__/plugin.test.ts +++ b/packages/sdk/src/vitest/plugin.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { isAbsolute } from "node:path"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import { createBlockPlugin, createEnvironmentPlugin } from "../plugin"; +import { createBlockPlugin, createEnvironmentPlugin } from "./plugin"; type ImportNode = { type: "ImportDeclaration" | "ExportNamedDeclaration" | "ExportAllDeclaration"; diff --git a/packages/sdk/src/vitest/__tests__/setup.test.ts b/packages/sdk/src/vitest/setup.test.ts similarity index 99% rename from packages/sdk/src/vitest/__tests__/setup.test.ts rename to packages/sdk/src/vitest/setup.test.ts index 55c1cda5f..4c5367bf6 100644 --- a/packages/sdk/src/vitest/__tests__/setup.test.ts +++ b/packages/sdk/src/vitest/setup.test.ts @@ -8,7 +8,7 @@ import { loadSecretsFromConfig, removeBlockedGlobals, restoreBlockedGlobals, -} from "../setup"; +} from "./setup"; describe("extractVaultStore", () => { test("unwraps a defineSecretManager() shape via the .vaults field", () => { diff --git a/packages/sdk/vitest.config.ts b/packages/sdk/vitest.config.ts index 14d2503df..3d681b437 100644 --- a/packages/sdk/vitest.config.ts +++ b/packages/sdk/vitest.config.ts @@ -16,15 +16,14 @@ export default defineConfig({ extends: true, test: { name: "unit", - include: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"], + include: ["**/?(*.)+(spec|test).ts"], exclude: [ "**/node_modules/**", "**/dist/**", "e2e/**", "**/__test_fixtures__/**", - "**/__tests__/fixtures/**", "src/plugin/compat.test.ts", - "src/vitest/__tests__/integration/**", + "src/vitest/integration/**", ], }, }, From bbbd42d1e9440a229af737a4ff05f505bfb39f32 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 19 May 2026 17:13:05 +0900 Subject: [PATCH 24/35] refactor(sdk): consolidate runtime globals to var tailor + namespace Tailor - Rewrite globals.ts to use `var tailor: TailorRuntime` + `namespace Tailor` type-only views, matching the existing `Tailordb` pattern. - Drop the `runtime` lazy accessor from `internal.ts`; each wrapper now uses a per-call typed cast on `globalThis` (mirrors `authconnection.ts`). - Update internal type-position references from `tailor.context.Invoker` to `Tailor.context.Invoker`. - As a side effect the ambient `triggerWorkflow` signature now mirrors `TailorWorkflowAPI` (camelCase params). --- .../services/workflow/wait-point.test.ts | 2 +- packages/sdk/src/runtime/authconnection.ts | 6 +- packages/sdk/src/runtime/context.ts | 4 +- packages/sdk/src/runtime/file.ts | 46 +++- packages/sdk/src/runtime/globals.test.ts | 4 +- packages/sdk/src/runtime/globals.ts | 242 +++--------------- packages/sdk/src/runtime/iconv.ts | 28 +- packages/sdk/src/runtime/idp.ts | 4 +- packages/sdk/src/runtime/internal.ts | 50 +--- packages/sdk/src/runtime/secretmanager.ts | 10 +- packages/sdk/src/runtime/workflow.ts | 21 +- packages/sdk/src/utils/test/mock.ts | 4 +- packages/sdk/src/vitest/mock-types.test.ts | 2 +- 13 files changed, 139 insertions(+), 284 deletions(-) diff --git a/packages/sdk/src/configure/services/workflow/wait-point.test.ts b/packages/sdk/src/configure/services/workflow/wait-point.test.ts index 37378a711..0849ed065 100644 --- a/packages/sdk/src/configure/services/workflow/wait-point.test.ts +++ b/packages/sdk/src/configure/services/workflow/wait-point.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, it, expect, expectTypeOf } from "vitest"; import { setupWaitPointMock, setupWorkflowMock } from "@/utils/test/mock"; import { defineWaitPoint, defineWaitPoints } from "./wait-point"; -const TailorGlobal = globalThis as { tailor?: { workflow?: Record } }; +const TailorGlobal = globalThis as { tailor?: unknown }; describe("defineWaitPoints", () => { afterEach(() => { diff --git a/packages/sdk/src/runtime/authconnection.ts b/packages/sdk/src/runtime/authconnection.ts index cc79c19a7..55b3e663e 100644 --- a/packages/sdk/src/runtime/authconnection.ts +++ b/packages/sdk/src/runtime/authconnection.ts @@ -12,8 +12,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { runtime } from "./internal"; - /** * Platform API surface for `tailor.authconnection`. Describes the shape the * platform runtime injects on `globalThis.tailor.authconnection`. @@ -30,5 +28,7 @@ export interface TailorAuthconnectionAPI { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function getConnectionToken(connectionName: string): Promise { - return runtime.tailor.authconnection.getConnectionToken(connectionName); + return ( + globalThis as { tailor: { authconnection: TailorAuthconnectionAPI } } + ).tailor.authconnection.getConnectionToken(connectionName); } diff --git a/packages/sdk/src/runtime/context.ts b/packages/sdk/src/runtime/context.ts index 98a82541a..5fac684a5 100644 --- a/packages/sdk/src/runtime/context.ts +++ b/packages/sdk/src/runtime/context.ts @@ -12,8 +12,6 @@ * } */ -import { runtime } from "./internal"; - /** * Information about the invoker of the current function execution. * @@ -66,7 +64,7 @@ export interface TailorContextAPI { * @returns Invoker details, or `null` when the call is anonymous */ export function getInvoker(): Invoker | null { - const raw = runtime.tailor.context.getInvoker(); + const raw = (globalThis as { tailor: { context: TailorContextAPI } }).tailor.context.getInvoker(); if (!raw) return null; return { id: raw.id, diff --git a/packages/sdk/src/runtime/file.ts b/packages/sdk/src/runtime/file.ts index 9f61d40b7..da3058c55 100644 --- a/packages/sdk/src/runtime/file.ts +++ b/packages/sdk/src/runtime/file.ts @@ -16,8 +16,6 @@ * ); */ -import { runtime } from "./internal"; - /** Upload response metadata. */ export interface UploadMetadata { fileSize: number; @@ -169,7 +167,14 @@ export function upload( data: string | ArrayBuffer | Uint8Array | number[], options?: FileUploadOptions, ): Promise { - return runtime.tailordb.file.upload(namespace, typeName, fieldName, recordId, data, options); + return (globalThis as { tailordb: { file: TailorDBFileAPI } }).tailordb.file.upload( + namespace, + typeName, + fieldName, + recordId, + data, + options, + ); } /** @@ -189,7 +194,12 @@ export function download( fieldName: string, recordId: string, ): Promise { - return runtime.tailordb.file.download(namespace, typeName, fieldName, recordId); + return (globalThis as { tailordb: { file: TailorDBFileAPI } }).tailordb.file.download( + namespace, + typeName, + fieldName, + recordId, + ); } /** @@ -209,7 +219,12 @@ export function downloadAsBase64( fieldName: string, recordId: string, ): Promise { - return runtime.tailordb.file.downloadAsBase64(namespace, typeName, fieldName, recordId); + return (globalThis as { tailordb: { file: TailorDBFileAPI } }).tailordb.file.downloadAsBase64( + namespace, + typeName, + fieldName, + recordId, + ); } /** @@ -226,7 +241,12 @@ function deleteFile( fieldName: string, recordId: string, ): Promise { - return runtime.tailordb.file.delete(namespace, typeName, fieldName, recordId); + return (globalThis as { tailordb: { file: TailorDBFileAPI } }).tailordb.file.delete( + namespace, + typeName, + fieldName, + recordId, + ); } /** @@ -243,7 +263,12 @@ export function getMetadata( fieldName: string, recordId: string, ): Promise { - return runtime.tailordb.file.getMetadata(namespace, typeName, fieldName, recordId); + return (globalThis as { tailordb: { file: TailorDBFileAPI } }).tailordb.file.getMetadata( + namespace, + typeName, + fieldName, + recordId, + ); } /** @@ -260,7 +285,12 @@ export function openDownloadStream( fieldName: string, recordId: string, ): Promise { - return runtime.tailordb.file.openDownloadStream(namespace, typeName, fieldName, recordId); + return (globalThis as { tailordb: { file: TailorDBFileAPI } }).tailordb.file.openDownloadStream( + namespace, + typeName, + fieldName, + recordId, + ); } export { deleteFile as delete }; diff --git a/packages/sdk/src/runtime/globals.test.ts b/packages/sdk/src/runtime/globals.test.ts index 5acb283c8..68542cbf9 100644 --- a/packages/sdk/src/runtime/globals.test.ts +++ b/packages/sdk/src/runtime/globals.test.ts @@ -26,8 +26,8 @@ describe("@tailor-platform/sdk/runtime/globals activates ambient globals", () => >(); }); - test("tailor.context.Invoker is exposed as a namespace type", () => { - expectTypeOf().not.toBeAny(); + test("Tailor.context.Invoker is exposed as a namespace type", () => { + expectTypeOf().not.toBeAny(); }); test("tailordb.file.upload is declared as a function", () => { diff --git a/packages/sdk/src/runtime/globals.ts b/packages/sdk/src/runtime/globals.ts index 9b3009643..96b58921a 100644 --- a/packages/sdk/src/runtime/globals.ts +++ b/packages/sdk/src/runtime/globals.ts @@ -14,12 +14,18 @@ * * Most users do not need to import this directly — `@tailor-platform/sdk/runtime` * exposes typed wrappers that cover the same surface without relying on globals. + * + * The value declarations (`var tailor` / `var tailordb`) are typed via the + * `TailorRuntime` / `TailordbRuntime` aggregates in `./internal`, which in turn + * compose the per-service `TailorXxxAPI` types declared alongside each wrapper. + * Namespaces `Tailor` / `Tailordb` are kept as type-only views so callers can + * still write `Tailor.idp.User` or `Tailor.context.Invoker` in type position. */ -/* eslint-disable @typescript-eslint/no-namespace, @typescript-eslint/no-explicit-any, jsdoc/require-param, jsdoc/require-returns, jsdoc/require-param-description */ +/* eslint-disable @typescript-eslint/no-namespace */ import type { ContextInvoker } from "./context"; -import type { TailorDBFileAPI, TailorDBFileErrorCode } from "./file"; +import type { TailorDBFileErrorCode } from "./file"; import type { ClientConfig as IdpClientConfig, CreateUserInput as IdpCreateUserInput, @@ -30,7 +36,12 @@ import type { User as IdpUser, UserQuery as IdpUserQuery, } from "./idp"; -import type { TailordbCommandType, TailordbQueryResult } from "./internal"; +import type { + TailordbCommandType, + TailordbQueryResult, + TailordbRuntime, + TailorRuntime, +} from "./internal"; import type { AuthInvoker as WorkflowAuthInvoker, TriggerWorkflowOptions as WorkflowTriggerWorkflowOptions, @@ -38,111 +49,38 @@ import type { declare global { namespace Tailordb { - class Client { - constructor(config: { namespace: string }); - connect(): Promise; - end(): Promise; - queryObject(sql: string, args?: readonly unknown[]): Promise>; - } - type QueryResult = TailordbQueryResult; type CommandType = TailordbCommandType; } // eslint-disable-next-line no-var - var tailordb: { - Client: typeof Tailordb.Client; - file: TailorDBFileAPI; - }; - - namespace tailor.secretmanager { - /** - * getSecrets returns multiple secret objects (key = name, value = secret) - * at once according to vault and secret names. - * - * If a secret does not exist, it will not be included in the result. - * @param vault - * @param names - */ - function getSecrets( - vault: string, - names: T, - ): Promise>>; - - /** - * getSecret returns a secret according to vault and name. - * - * If the secret does not exist, undefined is returned. - * @param vault - * @param name - */ - function getSecret(vault: string, name: string): Promise; - } - - namespace tailor.authconnection { - /** - * getConnectionToken returns the access token for an auth connection - * @param connectionName - */ - function getConnectionToken(connectionName: string): Promise; - } - - namespace tailor.iconv { - /** - * Convert string from one encoding to another - * @param str - * @param fromEncoding - * @param toEncoding - */ - function convert( - str: string | Uint8Array | ArrayBuffer, - fromEncoding: string, - toEncoding: T, - ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; - - /** - * Convert buffer from one encoding to another - * @param buffer - * @param fromEncoding - * @param toEncoding - */ - function convertBuffer( - buffer: Uint8Array | ArrayBuffer, - fromEncoding: string, - toEncoding: T, - ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; - - /** - * Decode buffer to string - * @param buffer - * @param encoding - */ - function decode(buffer: Uint8Array | ArrayBuffer, encoding: string): string; - - /** - * Encode string to buffer - * @param str - * @param encoding - */ - function encode( - str: string, - encoding: T, - ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; + var tailordb: TailordbRuntime; + + namespace Tailor { + namespace idp { + type ClientConfig = IdpClientConfig; + type User = IdpUser; + type UserQuery = IdpUserQuery; + type ListUsersOptions = IdpListUsersOptions; + type ListUsersResponse = IdpListUsersResponse; + type CreateUserInput = IdpCreateUserInput; + type UpdateUserInput = IdpUpdateUserInput; + type SendPasswordResetEmailInput = IdpSendPasswordResetEmailInput; + } - /** - * Get list of supported encodings - */ - function encodings(): string[]; + namespace workflow { + type AuthInvoker = WorkflowAuthInvoker; + type TriggerWorkflowOptions = WorkflowTriggerWorkflowOptions; + } - /** - * Iconv class for compatibility with node-iconv - */ - class Iconv { - constructor(fromEncoding: string, toEncoding: string); - convert(input: string | Uint8Array | ArrayBuffer): string | Uint8Array; + namespace context { + type Invoker = ContextInvoker; } } + // eslint-disable-next-line no-var + var tailor: TailorRuntime; + /** Custom error class for TailorDB File operations. */ class TailorDBFileError extends Error { constructor(message: string, code?: TailorDBFileErrorCode, cause?: unknown); @@ -175,114 +113,6 @@ declare global { constructor(message: string); name: "TailorErrorMessage"; } - - namespace tailor.idp { - type ClientConfig = IdpClientConfig; - type User = IdpUser; - type UserQuery = IdpUserQuery; - type ListUsersOptions = IdpListUsersOptions; - type ListUsersResponse = IdpListUsersResponse; - type CreateUserInput = IdpCreateUserInput; - type UpdateUserInput = IdpUpdateUserInput; - type SendPasswordResetEmailInput = IdpSendPasswordResetEmailInput; - - /** - * IDP Client for user management operations - */ - class Client { - constructor(config: ClientConfig); - - /** - * List users in the namespace with optional filtering and pagination. - */ - users(options?: ListUsersOptions): Promise; - - /** - * Get a user by ID. - */ - user(userId: string): Promise; - - /** - * Get a user by name. - */ - userByName(name: string): Promise; - - /** - * Create a new user. - */ - createUser(input: CreateUserInput): Promise; - - /** - * Update an existing user. - */ - updateUser(input: UpdateUserInput): Promise; - - /** - * Delete a user by ID. - * @returns True if successful - */ - deleteUser(userId: string): Promise; - - /** - * Send a password reset email to a user. - * @returns True if successful - */ - sendPasswordResetEmail(input: SendPasswordResetEmailInput): Promise; - } - } - - namespace tailor.workflow { - type AuthInvoker = WorkflowAuthInvoker; - type TriggerWorkflowOptions = WorkflowTriggerWorkflowOptions; - - /** - * Triggers a workflow and returns its execution ID. - * @param workflow_name - * @param args - * @param options - */ - function triggerWorkflow( - workflow_name: string, - args?: any, - options?: TriggerWorkflowOptions, - ): Promise; - - /** - * Triggers a job function and returns its result. - * @param job_name - * @param args - */ - function triggerJobFunction(job_name: string, args?: any): any; - - /** - * Suspends the current workflow execution and waits for an external signal to resume. - * @param key - * @param payload - */ - function wait(key: string, payload?: any): any; - - /** - * Resolves a waiting workflow execution, causing it to resume. - * @param executionId - * @param key - * @param callback - */ - function resolve( - executionId: string, - key: string, - callback: (waitPayload: any) => any, - ): Promise; - } - - namespace tailor.context { - type Invoker = ContextInvoker; - - /** - * Returns information about the invoker of the current function execution, - * or `null` for anonymous invocations. - */ - function getInvoker(): Invoker | null; - } } export {}; diff --git a/packages/sdk/src/runtime/iconv.ts b/packages/sdk/src/runtime/iconv.ts index cbbb89b42..e3736d649 100644 --- a/packages/sdk/src/runtime/iconv.ts +++ b/packages/sdk/src/runtime/iconv.ts @@ -17,8 +17,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { runtime } from "./internal"; - /** Instance methods exposed by `tailor.iconv.Iconv`. */ export interface IconvInstance { convert(input: string | Uint8Array | ArrayBuffer): string | Uint8Array; @@ -67,7 +65,11 @@ export function convert( fromEncoding: string, toEncoding: T, ): T extends "UTF8" | "UTF-8" ? string : Uint8Array { - return runtime.tailor.iconv.convert(str, fromEncoding, toEncoding); + return (globalThis as { tailor: { iconv: TailorIconvAPI } }).tailor.iconv.convert( + str, + fromEncoding, + toEncoding, + ); } /** @@ -82,7 +84,11 @@ export function convertBuffer( fromEncoding: string, toEncoding: T, ): T extends "UTF8" | "UTF-8" ? string : Uint8Array { - return runtime.tailor.iconv.convertBuffer(buffer, fromEncoding, toEncoding); + return (globalThis as { tailor: { iconv: TailorIconvAPI } }).tailor.iconv.convertBuffer( + buffer, + fromEncoding, + toEncoding, + ); } /** @@ -92,7 +98,10 @@ export function convertBuffer( * @returns Decoded UTF-8 string */ export function decode(buffer: Uint8Array | ArrayBuffer, encoding: string): string { - return runtime.tailor.iconv.decode(buffer, encoding); + return (globalThis as { tailor: { iconv: TailorIconvAPI } }).tailor.iconv.decode( + buffer, + encoding, + ); } /** @@ -105,7 +114,7 @@ export function encode( str: string, encoding: T, ): T extends "UTF8" | "UTF-8" ? string : Uint8Array { - return runtime.tailor.iconv.encode(str, encoding); + return (globalThis as { tailor: { iconv: TailorIconvAPI } }).tailor.iconv.encode(str, encoding); } /** @@ -113,7 +122,7 @@ export function encode( * @returns Array of encoding names supported by the platform iconv runtime */ export function encodings(): string[] { - return runtime.tailor.iconv.encodings(); + return (globalThis as { tailor: { iconv: TailorIconvAPI } }).tailor.iconv.encodings(); } /** @@ -124,7 +133,10 @@ export class Iconv { private impl: IconvInstance; constructor(fromEncoding: string, toEncoding: string) { - this.impl = new runtime.tailor.iconv.Iconv(fromEncoding, toEncoding); + this.impl = new (globalThis as { tailor: { iconv: TailorIconvAPI } }).tailor.iconv.Iconv( + fromEncoding, + toEncoding, + ); } /** diff --git a/packages/sdk/src/runtime/idp.ts b/packages/sdk/src/runtime/idp.ts index eacd67c41..22cf1b01e 100644 --- a/packages/sdk/src/runtime/idp.ts +++ b/packages/sdk/src/runtime/idp.ts @@ -11,8 +11,6 @@ * const { users } = await client.users({ first: 10 }); */ -import { runtime } from "./internal"; - /** Configuration object for {@link Client}. */ export interface ClientConfig { namespace: string; @@ -128,7 +126,7 @@ export class Client { #impl: IdpClientInstance; constructor(config: ClientConfig) { - this.#impl = new runtime.tailor.idp.Client(config); + this.#impl = new (globalThis as { tailor: { idp: TailorIdpAPI } }).tailor.idp.Client(config); } /** diff --git a/packages/sdk/src/runtime/internal.ts b/packages/sdk/src/runtime/internal.ts index 2232efe7d..36e471a7a 100644 --- a/packages/sdk/src/runtime/internal.ts +++ b/packages/sdk/src/runtime/internal.ts @@ -1,15 +1,15 @@ /** - * Internal runtime bindings shared by the typed wrappers in + * Internal aggregate types shared by the typed wrappers in * `@tailor-platform/sdk/runtime/*`. Not part of the public API. * - * - The exported `runtime` value reads `tailor` / `tailordb` from `globalThis` - * lazily through getters so wrappers stay decoupled from module-load order - * (mocks injected in `beforeEach` are picked up on next access). - * - The exported `TailorRuntime` / `TailordbRuntime` types aggregate the - * per-service API surfaces (which live alongside their wrappers in - * `./iconv`, `./idp`, etc.) into a single global shape. Importing this - * module does not introduce any ambient global declarations; the - * `declare global` block lives only in `./globals`. + * Each wrapper (`./iconv`, `./idp`, ...) exports its own `TailorXxxAPI` + * describing the slice of the runtime it consumes. This module assembles those + * slices into the full top-level shape so readers have a single overview, and + * so the ambient `var tailor` / `var tailordb` declarations in `./globals` can + * reuse the same type without inlining the union. + * + * Importing this module does NOT introduce any ambient global declarations; + * the `declare global` block lives only in `./globals`. * @internal */ @@ -23,7 +23,7 @@ import type { TailorWorkflowAPI } from "./workflow"; // --------------------------------------------------------------------------- // Tailordb client types — no service wrapper exists for the SQL Client, so -// these live here alongside the runtime accessor. +// these live here alongside the aggregate runtime types. // --------------------------------------------------------------------------- /** SQL command type recorded on a {@link TailordbQueryResult}. */ @@ -57,7 +57,9 @@ export interface TailordbClientConstructor { } // --------------------------------------------------------------------------- -// Top-level runtime shape — aggregates each service's API surface +// Top-level runtime shapes — aggregate each service's API surface so the +// ambient `var tailor` / `var tailordb` declarations in `./globals` and any +// callers that need the full shape can refer to a single name. // --------------------------------------------------------------------------- /** Top-level `tailor` runtime object. */ @@ -75,29 +77,3 @@ export interface TailordbRuntime { Client: TailordbClientConstructor; file: TailorDBFileAPI; } - -// --------------------------------------------------------------------------- -// Lazy typed accessor — reads `tailor` / `tailordb` from globalThis on every -// property access so test setups that swap globals in `beforeEach` are picked -// up without re-importing. Importing this value does NOT activate any ambient -// global declarations. -// --------------------------------------------------------------------------- - -interface RuntimeBindings { - readonly tailor: TailorRuntime; - readonly tailordb: TailordbRuntime; -} - -/** - * Lazy typed view of the platform runtime globals (`tailor`, `tailordb`). - * Each property read returns the current `globalThis` value, so test setups - * that inject mocks in `beforeEach` work without needing to re-import. - */ -export const runtime: RuntimeBindings = { - get tailor() { - return (globalThis as unknown as { tailor: TailorRuntime }).tailor; - }, - get tailordb() { - return (globalThis as unknown as { tailordb: TailordbRuntime }).tailordb; - }, -}; diff --git a/packages/sdk/src/runtime/secretmanager.ts b/packages/sdk/src/runtime/secretmanager.ts index a49f00430..92dee2252 100644 --- a/packages/sdk/src/runtime/secretmanager.ts +++ b/packages/sdk/src/runtime/secretmanager.ts @@ -12,8 +12,6 @@ * const all = await secretmanager.getSecrets("my-vault", ["A", "B"] as const); */ -import { runtime } from "./internal"; - /** * Platform API surface for `tailor.secretmanager`. Describes the shape the * platform runtime injects on `globalThis.tailor.secretmanager`. @@ -37,7 +35,9 @@ export function getSecrets( vault: string, names: T, ): Promise>> { - return runtime.tailor.secretmanager.getSecrets(vault, names); + return ( + globalThis as { tailor: { secretmanager: TailorSecretmanagerAPI } } + ).tailor.secretmanager.getSecrets(vault, names); } /** @@ -47,5 +47,7 @@ export function getSecrets( * @returns The secret value, or `undefined` if not present */ export function getSecret(vault: string, name: string): Promise { - return runtime.tailor.secretmanager.getSecret(vault, name); + return ( + globalThis as { tailor: { secretmanager: TailorSecretmanagerAPI } } + ).tailor.secretmanager.getSecret(vault, name); } diff --git a/packages/sdk/src/runtime/workflow.ts b/packages/sdk/src/runtime/workflow.ts index 9db657075..37cec022c 100644 --- a/packages/sdk/src/runtime/workflow.ts +++ b/packages/sdk/src/runtime/workflow.ts @@ -12,8 +12,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { runtime } from "./internal"; - /** * Specifies the machine user that should be used to execute the workflow. * This allows workflows to run with specific authentication context. @@ -60,7 +58,9 @@ export function triggerWorkflow( args?: any, options?: TriggerWorkflowOptions, ): Promise { - return runtime.tailor.workflow.triggerWorkflow(workflowName, args, options); + return ( + globalThis as { tailor: { workflow: TailorWorkflowAPI } } + ).tailor.workflow.triggerWorkflow(workflowName, args, options); } /** @@ -71,7 +71,9 @@ export function triggerWorkflow( */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function triggerJobFunction(jobName: string, args?: any): any { - return runtime.tailor.workflow.triggerJobFunction(jobName, args); + return ( + globalThis as { tailor: { workflow: TailorWorkflowAPI } } + ).tailor.workflow.triggerJobFunction(jobName, args); } /** @@ -82,7 +84,10 @@ export function triggerJobFunction(jobName: string, args?: any): any { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function wait(key: string, payload?: any): any { - return runtime.tailor.workflow.wait(key, payload); + return (globalThis as { tailor: { workflow: TailorWorkflowAPI } }).tailor.workflow.wait( + key, + payload, + ); } /** @@ -98,5 +103,9 @@ export function resolve( // eslint-disable-next-line @typescript-eslint/no-explicit-any callback: (waitPayload: any) => any, ): Promise { - return runtime.tailor.workflow.resolve(executionId, key, callback); + return (globalThis as { tailor: { workflow: TailorWorkflowAPI } }).tailor.workflow.resolve( + executionId, + key, + callback, + ); } diff --git a/packages/sdk/src/utils/test/mock.ts b/packages/sdk/src/utils/test/mock.ts index cb52d0a74..bb6da0ac1 100644 --- a/packages/sdk/src/utils/test/mock.ts +++ b/packages/sdk/src/utils/test/mock.ts @@ -34,7 +34,7 @@ interface TailordbGlobal { ) => Promise; }; context: { - getInvoker: () => tailor.context.Invoker | null; + getInvoker: () => Tailor.context.Invoker | null; }; }; } @@ -136,7 +136,7 @@ export function setupWorkflowMock(handler: JobHandler): { * @param invoker - The `TailorInvoker` value to return, or `null` for anonymous. */ export function setupInvokerMock(invoker: TailorInvoker): void { - const raw: tailor.context.Invoker | null = invoker + const raw: Tailor.context.Invoker | null = invoker ? { id: invoker.id, type: invoker.type, diff --git a/packages/sdk/src/vitest/mock-types.test.ts b/packages/sdk/src/vitest/mock-types.test.ts index ce6c1778b..ade8492b9 100644 --- a/packages/sdk/src/vitest/mock-types.test.ts +++ b/packages/sdk/src/vitest/mock-types.test.ts @@ -36,7 +36,7 @@ describe("mock types match @tailor-platform/sdk/runtime/globals", () => { describe("tailor.context", () => { test("getInvoker returns Invoker | null", () => { - expectTypeOf(tailor.context.getInvoker()).toEqualTypeOf(); + expectTypeOf(tailor.context.getInvoker()).toEqualTypeOf(); }); }); }); From f9723e5767f50cbaa41afb0a25bfd4b03289cc55 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 19 May 2026 17:35:27 +0900 Subject: [PATCH 25/35] fix(sdk): avoid ambient Tailor namespace in @tailor-platform/sdk/test `utils/test/mock.ts` referenced `Tailor.context.Invoker`, but the declaration banner is intentionally stripped from every emitted `.d.mts` except `configure/index.d.mts`. Consumers importing `@tailor-platform/sdk/test` would therefore see "Cannot find namespace 'Tailor'" unless they also opted into `@tailor-platform/sdk/runtime/globals`. Switch to the non-ambient `ContextInvoker` type from `@/runtime/context` so the `./test` subpath stays self-contained. --- packages/sdk/src/utils/test/mock.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/utils/test/mock.ts b/packages/sdk/src/utils/test/mock.ts index bb6da0ac1..0150a476c 100644 --- a/packages/sdk/src/utils/test/mock.ts +++ b/packages/sdk/src/utils/test/mock.ts @@ -1,5 +1,6 @@ import * as path from "node:path"; import { pathToFileURL } from "node:url"; +import type { ContextInvoker } from "@/runtime/context"; import type { TailorInvoker } from "@/types/user"; type MainFunction = (args: Record) => unknown | Promise; @@ -34,7 +35,7 @@ interface TailordbGlobal { ) => Promise; }; context: { - getInvoker: () => Tailor.context.Invoker | null; + getInvoker: () => ContextInvoker | null; }; }; } @@ -136,7 +137,7 @@ export function setupWorkflowMock(handler: JobHandler): { * @param invoker - The `TailorInvoker` value to return, or `null` for anonymous. */ export function setupInvokerMock(invoker: TailorInvoker): void { - const raw: Tailor.context.Invoker | null = invoker + const raw: ContextInvoker | null = invoker ? { id: invoker.id, type: invoker.type, From 4567e8996d6b64fa1bdf0d5919a270fb5fdfd3be Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 19 May 2026 17:52:18 +0900 Subject: [PATCH 26/35] refactor(sdk): inline runtime aggregates into index.ts and drop internal.ts --- packages/sdk/src/runtime/globals.ts | 16 +++--- packages/sdk/src/runtime/index.ts | 59 ++++++++++++++++++++- packages/sdk/src/runtime/internal.ts | 79 ---------------------------- 3 files changed, 64 insertions(+), 90 deletions(-) delete mode 100644 packages/sdk/src/runtime/internal.ts diff --git a/packages/sdk/src/runtime/globals.ts b/packages/sdk/src/runtime/globals.ts index 96b58921a..b3c1171f4 100644 --- a/packages/sdk/src/runtime/globals.ts +++ b/packages/sdk/src/runtime/globals.ts @@ -16,14 +16,16 @@ * exposes typed wrappers that cover the same surface without relying on globals. * * The value declarations (`var tailor` / `var tailordb`) are typed via the - * `TailorRuntime` / `TailordbRuntime` aggregates in `./internal`, which in turn - * compose the per-service `TailorXxxAPI` types declared alongside each wrapper. - * Namespaces `Tailor` / `Tailordb` are kept as type-only views so callers can - * still write `Tailor.idp.User` or `Tailor.context.Invoker` in type position. + * `TailorRuntime` / `TailordbRuntime` aggregates re-exported from `.`, which in + * turn compose the per-service `TailorXxxAPI` types declared alongside each + * wrapper. Namespaces `Tailor` / `Tailordb` are kept as type-only views so + * callers can still write `Tailor.idp.User` or `Tailor.context.Invoker` in + * type position. */ /* eslint-disable @typescript-eslint/no-namespace */ +import type { TailordbCommandType, TailordbQueryResult, TailordbRuntime, TailorRuntime } from "."; import type { ContextInvoker } from "./context"; import type { TailorDBFileErrorCode } from "./file"; import type { @@ -36,12 +38,6 @@ import type { User as IdpUser, UserQuery as IdpUserQuery, } from "./idp"; -import type { - TailordbCommandType, - TailordbQueryResult, - TailordbRuntime, - TailorRuntime, -} from "./internal"; import type { AuthInvoker as WorkflowAuthInvoker, TriggerWorkflowOptions as WorkflowTriggerWorkflowOptions, diff --git a/packages/sdk/src/runtime/index.ts b/packages/sdk/src/runtime/index.ts index 743d7b3a2..e7e9c7a5c 100644 --- a/packages/sdk/src/runtime/index.ts +++ b/packages/sdk/src/runtime/index.ts @@ -2,7 +2,10 @@ * Typed wrappers for the Tailor Platform Function runtime APIs. * * Each namespace mirrors the corresponding `tailor.*` (or `tailordb.file`) - * surface that the platform runtime exposes globally. + * surface that the platform runtime exposes globally. The aggregate + * `TailorRuntime` / `TailordbRuntime` types compose those per-service shapes + * into the full top-level runtime objects, and are reused by the ambient + * `var tailor` / `var tailordb` declarations in `./globals`. * @example * import { iconv, secretmanager, idp, workflow, file } from "@tailor-platform/sdk/runtime"; * @@ -11,6 +14,14 @@ * const client = new idp.Client({ namespace: "my-namespace" }); */ +import type { TailorAuthconnectionAPI } from "./authconnection"; +import type { TailorContextAPI } from "./context"; +import type { TailorDBFileAPI } from "./file"; +import type { TailorIconvAPI } from "./iconv"; +import type { TailorIdpAPI } from "./idp"; +import type { TailorSecretmanagerAPI } from "./secretmanager"; +import type { TailorWorkflowAPI } from "./workflow"; + export * as iconv from "./iconv"; export * as secretmanager from "./secretmanager"; export * as authconnection from "./authconnection"; @@ -18,3 +29,49 @@ export * as idp from "./idp"; export * as workflow from "./workflow"; export * as context from "./context"; export * as file from "./file"; + +/** SQL command type recorded on a {@link TailordbQueryResult}. */ +export type TailordbCommandType = + | "INSERT" + | "DELETE" + | "UPDATE" + | "SELECT" + | "MOVE" + | "FETCH" + | "COPY" + | "CREATE"; + +/** Result of a single `queryObject` call against the TailorDB driver. */ +export interface TailordbQueryResult { + rows: T[]; + command: TailordbCommandType; + rowCount: number; +} + +/** Instance methods exposed by `tailordb.Client`. */ +export interface TailordbClientInstance { + connect(): Promise; + end(): Promise; + queryObject(sql: string, args?: readonly unknown[]): Promise>; +} + +/** Constructor shape for `tailordb.Client`. */ +export interface TailordbClientConstructor { + new (config: { namespace: string }): TailordbClientInstance; +} + +/** Top-level `tailor` runtime object. */ +export interface TailorRuntime { + secretmanager: TailorSecretmanagerAPI; + authconnection: TailorAuthconnectionAPI; + iconv: TailorIconvAPI; + idp: TailorIdpAPI; + workflow: TailorWorkflowAPI; + context: TailorContextAPI; +} + +/** Top-level `tailordb` runtime object. */ +export interface TailordbRuntime { + Client: TailordbClientConstructor; + file: TailorDBFileAPI; +} diff --git a/packages/sdk/src/runtime/internal.ts b/packages/sdk/src/runtime/internal.ts deleted file mode 100644 index 36e471a7a..000000000 --- a/packages/sdk/src/runtime/internal.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Internal aggregate types shared by the typed wrappers in - * `@tailor-platform/sdk/runtime/*`. Not part of the public API. - * - * Each wrapper (`./iconv`, `./idp`, ...) exports its own `TailorXxxAPI` - * describing the slice of the runtime it consumes. This module assembles those - * slices into the full top-level shape so readers have a single overview, and - * so the ambient `var tailor` / `var tailordb` declarations in `./globals` can - * reuse the same type without inlining the union. - * - * Importing this module does NOT introduce any ambient global declarations; - * the `declare global` block lives only in `./globals`. - * @internal - */ - -import type { TailorAuthconnectionAPI } from "./authconnection"; -import type { TailorContextAPI } from "./context"; -import type { TailorDBFileAPI } from "./file"; -import type { TailorIconvAPI } from "./iconv"; -import type { TailorIdpAPI } from "./idp"; -import type { TailorSecretmanagerAPI } from "./secretmanager"; -import type { TailorWorkflowAPI } from "./workflow"; - -// --------------------------------------------------------------------------- -// Tailordb client types — no service wrapper exists for the SQL Client, so -// these live here alongside the aggregate runtime types. -// --------------------------------------------------------------------------- - -/** SQL command type recorded on a {@link TailordbQueryResult}. */ -export type TailordbCommandType = - | "INSERT" - | "DELETE" - | "UPDATE" - | "SELECT" - | "MOVE" - | "FETCH" - | "COPY" - | "CREATE"; - -/** Result of a single `queryObject` call against the TailorDB driver. */ -export interface TailordbQueryResult { - rows: T[]; - command: TailordbCommandType; - rowCount: number; -} - -/** Instance methods exposed by `tailordb.Client`. */ -export interface TailordbClientInstance { - connect(): Promise; - end(): Promise; - queryObject(sql: string, args?: readonly unknown[]): Promise>; -} - -/** Constructor shape for `tailordb.Client`. */ -export interface TailordbClientConstructor { - new (config: { namespace: string }): TailordbClientInstance; -} - -// --------------------------------------------------------------------------- -// Top-level runtime shapes — aggregate each service's API surface so the -// ambient `var tailor` / `var tailordb` declarations in `./globals` and any -// callers that need the full shape can refer to a single name. -// --------------------------------------------------------------------------- - -/** Top-level `tailor` runtime object. */ -export interface TailorRuntime { - secretmanager: TailorSecretmanagerAPI; - authconnection: TailorAuthconnectionAPI; - iconv: TailorIconvAPI; - idp: TailorIdpAPI; - workflow: TailorWorkflowAPI; - context: TailorContextAPI; -} - -/** Top-level `tailordb` runtime object. */ -export interface TailordbRuntime { - Client: TailordbClientConstructor; - file: TailorDBFileAPI; -} From 9697f735e6ff70ea9aeb4c3aef6df9b8ceb1aa19 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 19 May 2026 20:41:00 +0900 Subject: [PATCH 27/35] refactor(sdk): merge type-only namespaces into var tailor / var tailordb --- packages/sdk/src/cli/shared/mock.ts | 4 ++-- packages/sdk/src/runtime/globals.test.ts | 4 ++-- packages/sdk/src/runtime/globals.ts | 10 +++++----- packages/sdk/src/vitest/mock-types.test.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/sdk/src/cli/shared/mock.ts b/packages/sdk/src/cli/shared/mock.ts index db50feaa1..dd6371938 100644 --- a/packages/sdk/src/cli/shared/mock.ts +++ b/packages/sdk/src/cli/shared/mock.ts @@ -10,8 +10,8 @@ constructor(_config: { namespace: string }) {} async connect(): Promise {} async end(): Promise {} - async queryObject(): Promise> { - return {} as Promise>; + async queryObject(): Promise> { + return {} as Promise>; } }, }; diff --git a/packages/sdk/src/runtime/globals.test.ts b/packages/sdk/src/runtime/globals.test.ts index 68542cbf9..5acb283c8 100644 --- a/packages/sdk/src/runtime/globals.test.ts +++ b/packages/sdk/src/runtime/globals.test.ts @@ -26,8 +26,8 @@ describe("@tailor-platform/sdk/runtime/globals activates ambient globals", () => >(); }); - test("Tailor.context.Invoker is exposed as a namespace type", () => { - expectTypeOf().not.toBeAny(); + test("tailor.context.Invoker is exposed as a namespace type", () => { + expectTypeOf().not.toBeAny(); }); test("tailordb.file.upload is declared as a function", () => { diff --git a/packages/sdk/src/runtime/globals.ts b/packages/sdk/src/runtime/globals.ts index b3c1171f4..48168a0c1 100644 --- a/packages/sdk/src/runtime/globals.ts +++ b/packages/sdk/src/runtime/globals.ts @@ -18,9 +18,9 @@ * The value declarations (`var tailor` / `var tailordb`) are typed via the * `TailorRuntime` / `TailordbRuntime` aggregates re-exported from `.`, which in * turn compose the per-service `TailorXxxAPI` types declared alongside each - * wrapper. Namespaces `Tailor` / `Tailordb` are kept as type-only views so - * callers can still write `Tailor.idp.User` or `Tailor.context.Invoker` in - * type position. + * wrapper. Type-only `namespace tailor` / `namespace tailordb` declarations + * are merged with those vars so callers can write `tailor.idp.User` or + * `tailor.context.Invoker` in type position as well. */ /* eslint-disable @typescript-eslint/no-namespace */ @@ -44,7 +44,7 @@ import type { } from "./workflow"; declare global { - namespace Tailordb { + namespace tailordb { type QueryResult = TailordbQueryResult; type CommandType = TailordbCommandType; } @@ -52,7 +52,7 @@ declare global { // eslint-disable-next-line no-var var tailordb: TailordbRuntime; - namespace Tailor { + namespace tailor { namespace idp { type ClientConfig = IdpClientConfig; type User = IdpUser; diff --git a/packages/sdk/src/vitest/mock-types.test.ts b/packages/sdk/src/vitest/mock-types.test.ts index ade8492b9..ce6c1778b 100644 --- a/packages/sdk/src/vitest/mock-types.test.ts +++ b/packages/sdk/src/vitest/mock-types.test.ts @@ -36,7 +36,7 @@ describe("mock types match @tailor-platform/sdk/runtime/globals", () => { describe("tailor.context", () => { test("getInvoker returns Invoker | null", () => { - expectTypeOf(tailor.context.getInvoker()).toEqualTypeOf(); + expectTypeOf(tailor.context.getInvoker()).toEqualTypeOf(); }); }); }); From 510684235d3a997af99212594b4cbc693bbb8fc9 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 19 May 2026 21:27:50 +0900 Subject: [PATCH 28/35] fix(sdk): export deleteFile alongside delete alias for named imports --- packages/sdk/src/runtime/file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/runtime/file.ts b/packages/sdk/src/runtime/file.ts index da3058c55..5a547a544 100644 --- a/packages/sdk/src/runtime/file.ts +++ b/packages/sdk/src/runtime/file.ts @@ -235,7 +235,7 @@ export function downloadAsBase64( * @param recordId - Record ID owning the field * @returns Resolves once the file has been deleted */ -function deleteFile( +export function deleteFile( namespace: string, typeName: string, fieldName: string, From ce83a92e7f0e5073fdd2c1874e476bd6c56d3aa0 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 19 May 2026 21:56:19 +0900 Subject: [PATCH 29/35] refactor(sdk): consolidate runtime wrappers via indexed access types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each runtime/.ts module previously declared full function signatures twice — once on the platform-API interface and again on each thin re-export. Move all JSDoc to the interface methods, then define each export as a one-line arrow function typed by TailorXxxAPI["method"]. This drops ~250 net lines across file.ts, iconv.ts, secretmanager.ts, authconnection.ts, and workflow.ts while keeping the public API identical. context.ts (transforms its payload) and idp.ts (Client class) are intentionally left as-is. --- packages/sdk/src/runtime/authconnection.ts | 25 ++- packages/sdk/src/runtime/file.ts | 206 +++++++++------------ packages/sdk/src/runtime/iconv.ts | 117 ++++++------ packages/sdk/src/runtime/secretmanager.ts | 47 +++-- packages/sdk/src/runtime/workflow.ts | 100 +++++----- 5 files changed, 238 insertions(+), 257 deletions(-) diff --git a/packages/sdk/src/runtime/authconnection.ts b/packages/sdk/src/runtime/authconnection.ts index 55b3e663e..741542e61 100644 --- a/packages/sdk/src/runtime/authconnection.ts +++ b/packages/sdk/src/runtime/authconnection.ts @@ -15,20 +15,27 @@ /** * Platform API surface for `tailor.authconnection`. Describes the shape the * platform runtime injects on `globalThis.tailor.authconnection`. - * @internal + * + * Each method below is also re-exported as a top-level named export from this + * module so callers can either `import * as authconnection from + * "@tailor-platform/sdk/runtime/authconnection"` or pick individual methods. */ export interface TailorAuthconnectionAPI { + /** + * Returns the access token for the given auth connection. + * @param connectionName - Auth connection name as defined in tailor.config + * @returns Token payload (provider-specific shape) + */ getConnectionToken(connectionName: string): Promise; } +const api = (): TailorAuthconnectionAPI => + (globalThis as { tailor: { authconnection: TailorAuthconnectionAPI } }).tailor.authconnection; + /** - * Returns the access token for the given auth connection. - * @param connectionName - Auth connection name as defined in tailor.config + * See {@link TailorAuthconnectionAPI.getConnectionToken}. + * @param args - Forwarded to {@link TailorAuthconnectionAPI.getConnectionToken} * @returns Token payload (provider-specific shape) */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function getConnectionToken(connectionName: string): Promise { - return ( - globalThis as { tailor: { authconnection: TailorAuthconnectionAPI } } - ).tailor.authconnection.getConnectionToken(connectionName); -} +export const getConnectionToken: TailorAuthconnectionAPI["getConnectionToken"] = (...args) => + api().getConnectionToken(...args); diff --git a/packages/sdk/src/runtime/file.ts b/packages/sdk/src/runtime/file.ts index 5a547a544..fe7ebfcad 100644 --- a/packages/sdk/src/runtime/file.ts +++ b/packages/sdk/src/runtime/file.ts @@ -106,9 +106,23 @@ export interface TailorDBFileError extends Error { /** * Platform API surface for `tailordb.file`. Describes the shape the platform * runtime injects on `globalThis.tailordb.file`. - * @internal + * + * Each method below is also re-exported as a top-level named export from this + * module (e.g. `upload`, `download`, `deleteFile`) so callers can either + * `import * as file from "@tailor-platform/sdk/runtime/file"` or pick + * individual methods. */ export interface TailorDBFileAPI { + /** + * Upload a file to TailorDB. + * @param namespace - TailorDB namespace + * @param typeName - TailorDB type name + * @param fieldName - File field name on the type + * @param recordId - Record ID owning the field + * @param data - File contents + * @param options - Upload options (e.g. `contentType`) + * @returns Upload response containing the file metadata + */ upload( namespace: string, typeName: string, @@ -118,6 +132,17 @@ export interface TailorDBFileAPI { options?: FileUploadOptions, ): Promise; + /** + * Download a file from TailorDB. + * + * Throws `TailorDBFileError` with code `FILE_TOO_LARGE` when the file + * exceeds 10MB — use {@link openDownloadStream} for large files. + * @param namespace - TailorDB namespace + * @param typeName - TailorDB type name + * @param fieldName - File field name on the type + * @param recordId - Record ID owning the field + * @returns Bytes and metadata for the file + */ download( namespace: string, typeName: string, @@ -125,6 +150,17 @@ export interface TailorDBFileAPI { recordId: string, ): Promise; + /** + * Download a file from TailorDB as a Base64-encoded string. + * + * Throws `TailorDBFileError` with code `FILE_TOO_LARGE` when the file + * exceeds 10MB — use {@link openDownloadStream} for large files. + * @param namespace - TailorDB namespace + * @param typeName - TailorDB type name + * @param fieldName - File field name on the type + * @param recordId - Record ID owning the field + * @returns Base64-encoded contents and metadata for the file + */ downloadAsBase64( namespace: string, typeName: string, @@ -132,8 +168,25 @@ export interface TailorDBFileAPI { recordId: string, ): Promise; + /** + * Delete a file from TailorDB. Exported as `deleteFile` (and aliased as + * `delete`) so it can be used both with named and namespace imports. + * @param namespace - TailorDB namespace + * @param typeName - TailorDB type name + * @param fieldName - File field name on the type + * @param recordId - Record ID owning the field + * @returns Resolves once the file has been deleted + */ delete(namespace: string, typeName: string, fieldName: string, recordId: string): Promise; + /** + * Get file metadata from TailorDB. + * @param namespace - TailorDB namespace + * @param typeName - TailorDB type name + * @param fieldName - File field name on the type + * @param recordId - Record ID owning the field + * @returns Metadata for the stored file + */ getMetadata( namespace: string, typeName: string, @@ -141,6 +194,14 @@ export interface TailorDBFileAPI { recordId: string, ): Promise; + /** + * Open a download stream for large files. + * @param namespace - TailorDB namespace + * @param typeName - TailorDB type name + * @param fieldName - File field name on the type + * @param recordId - Record ID owning the field + * @returns Async iterator yielding file chunks; call `close()` to release resources + */ openDownloadStream( namespace: string, typeName: string, @@ -149,148 +210,51 @@ export interface TailorDBFileAPI { ): Promise; } +const api = (): TailorDBFileAPI => + (globalThis as { tailordb: { file: TailorDBFileAPI } }).tailordb.file; + /** - * Upload a file to TailorDB. - * @param namespace - TailorDB namespace - * @param typeName - TailorDB type name - * @param fieldName - File field name on the type - * @param recordId - Record ID owning the field - * @param data - File contents - * @param options - Upload options (e.g. `contentType`) + * See {@link TailorDBFileAPI.upload}. + * @param args - Forwarded to {@link TailorDBFileAPI.upload} * @returns Upload response containing the file metadata */ -export function upload( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, - data: string | ArrayBuffer | Uint8Array | number[], - options?: FileUploadOptions, -): Promise { - return (globalThis as { tailordb: { file: TailorDBFileAPI } }).tailordb.file.upload( - namespace, - typeName, - fieldName, - recordId, - data, - options, - ); -} +export const upload: TailorDBFileAPI["upload"] = (...args) => api().upload(...args); /** - * Download a file from TailorDB. - * - * Throws `TailorDBFileError` with code `FILE_TOO_LARGE` when the file - * exceeds 10MB — use {@link openDownloadStream} for large files. - * @param namespace - TailorDB namespace - * @param typeName - TailorDB type name - * @param fieldName - File field name on the type - * @param recordId - Record ID owning the field + * See {@link TailorDBFileAPI.download}. + * @param args - Forwarded to {@link TailorDBFileAPI.download} * @returns Bytes and metadata for the file */ -export function download( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, -): Promise { - return (globalThis as { tailordb: { file: TailorDBFileAPI } }).tailordb.file.download( - namespace, - typeName, - fieldName, - recordId, - ); -} +export const download: TailorDBFileAPI["download"] = (...args) => api().download(...args); /** - * Download a file from TailorDB as a Base64-encoded string. - * - * Throws `TailorDBFileError` with code `FILE_TOO_LARGE` when the file - * exceeds 10MB — use {@link openDownloadStream} for large files. - * @param namespace - TailorDB namespace - * @param typeName - TailorDB type name - * @param fieldName - File field name on the type - * @param recordId - Record ID owning the field + * See {@link TailorDBFileAPI.downloadAsBase64}. + * @param args - Forwarded to {@link TailorDBFileAPI.downloadAsBase64} * @returns Base64-encoded contents and metadata for the file */ -export function downloadAsBase64( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, -): Promise { - return (globalThis as { tailordb: { file: TailorDBFileAPI } }).tailordb.file.downloadAsBase64( - namespace, - typeName, - fieldName, - recordId, - ); -} +export const downloadAsBase64: TailorDBFileAPI["downloadAsBase64"] = (...args) => + api().downloadAsBase64(...args); /** - * Delete a file from TailorDB. - * @param namespace - TailorDB namespace - * @param typeName - TailorDB type name - * @param fieldName - File field name on the type - * @param recordId - Record ID owning the field + * See {@link TailorDBFileAPI.delete}. + * @param args - Forwarded to {@link TailorDBFileAPI.delete} * @returns Resolves once the file has been deleted */ -export function deleteFile( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, -): Promise { - return (globalThis as { tailordb: { file: TailorDBFileAPI } }).tailordb.file.delete( - namespace, - typeName, - fieldName, - recordId, - ); -} +export const deleteFile: TailorDBFileAPI["delete"] = (...args) => api().delete(...args); /** - * Get file metadata from TailorDB. - * @param namespace - TailorDB namespace - * @param typeName - TailorDB type name - * @param fieldName - File field name on the type - * @param recordId - Record ID owning the field + * See {@link TailorDBFileAPI.getMetadata}. + * @param args - Forwarded to {@link TailorDBFileAPI.getMetadata} * @returns Metadata for the stored file */ -export function getMetadata( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, -): Promise { - return (globalThis as { tailordb: { file: TailorDBFileAPI } }).tailordb.file.getMetadata( - namespace, - typeName, - fieldName, - recordId, - ); -} +export const getMetadata: TailorDBFileAPI["getMetadata"] = (...args) => api().getMetadata(...args); /** - * Open a download stream for large files. - * @param namespace - TailorDB namespace - * @param typeName - TailorDB type name - * @param fieldName - File field name on the type - * @param recordId - Record ID owning the field + * See {@link TailorDBFileAPI.openDownloadStream}. + * @param args - Forwarded to {@link TailorDBFileAPI.openDownloadStream} * @returns Async iterator yielding file chunks; call `close()` to release resources */ -export function openDownloadStream( - namespace: string, - typeName: string, - fieldName: string, - recordId: string, -): Promise { - return (globalThis as { tailordb: { file: TailorDBFileAPI } }).tailordb.file.openDownloadStream( - namespace, - typeName, - fieldName, - recordId, - ); -} +export const openDownloadStream: TailorDBFileAPI["openDownloadStream"] = (...args) => + api().openDownloadStream(...args); export { deleteFile as delete }; diff --git a/packages/sdk/src/runtime/iconv.ts b/packages/sdk/src/runtime/iconv.ts index e3736d649..0e17aa594 100644 --- a/packages/sdk/src/runtime/iconv.ts +++ b/packages/sdk/src/runtime/iconv.ts @@ -31,99 +31,105 @@ export interface IconvConstructor { * Platform API surface for `tailor.iconv`. Describes the shape the platform * runtime injects on `globalThis.tailor.iconv` so the wrapper and ambient * globals stay in sync. - * @internal + * + * Each method below is also re-exported as a top-level named export from this + * module (e.g. `convert`, `decode`, `encode`) so callers can either + * `import * as iconv from "@tailor-platform/sdk/runtime/iconv"` or pick + * individual methods. */ export interface TailorIconvAPI { + /** + * Convert a string or buffer between encodings. + * @param str - Input data to convert + * @param fromEncoding - Source encoding name + * @param toEncoding - Target encoding name + * @returns `string` when `toEncoding` is `"UTF8"` or `"UTF-8"`, otherwise `Uint8Array`. + */ convert( str: string | Uint8Array | ArrayBuffer, fromEncoding: string, toEncoding: T, ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; + + /** + * Convert a buffer between encodings. + * @param buffer - Input bytes to convert + * @param fromEncoding - Source encoding name + * @param toEncoding - Target encoding name + * @returns `string` when `toEncoding` is `"UTF8"` or `"UTF-8"`, otherwise `Uint8Array`. + */ convertBuffer( buffer: Uint8Array | ArrayBuffer, fromEncoding: string, toEncoding: T, ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; + + /** + * Decode a buffer to a UTF-8 string using the given source encoding. + * @param buffer - Input bytes + * @param encoding - Source encoding name + * @returns Decoded UTF-8 string + */ decode(buffer: Uint8Array | ArrayBuffer, encoding: string): string; + + /** + * Encode a UTF-8 string into the given target encoding. + * @param str - Input string + * @param encoding - Target encoding name + * @returns `string` when `encoding` is `"UTF8"` or `"UTF-8"`, otherwise `Uint8Array`. + */ encode( str: string, encoding: T, ): T extends "UTF8" | "UTF-8" ? string : Uint8Array; + + /** + * Returns the list of supported encoding names. + * @returns Array of encoding names supported by the platform iconv runtime + */ encodings(): string[]; + + /** Constructor for the stateful {@link Iconv} converter. */ Iconv: IconvConstructor; } +const api = (): TailorIconvAPI => + (globalThis as { tailor: { iconv: TailorIconvAPI } }).tailor.iconv; + /** - * Convert a string or buffer between encodings. - * @param str - Input data to convert - * @param fromEncoding - Source encoding name - * @param toEncoding - Target encoding name + * See {@link TailorIconvAPI.convert}. + * @param args - Forwarded to {@link TailorIconvAPI.convert} * @returns `string` when `toEncoding` is `"UTF8"` or `"UTF-8"`, otherwise `Uint8Array`. */ -export function convert( - str: string | Uint8Array | ArrayBuffer, - fromEncoding: string, - toEncoding: T, -): T extends "UTF8" | "UTF-8" ? string : Uint8Array { - return (globalThis as { tailor: { iconv: TailorIconvAPI } }).tailor.iconv.convert( - str, - fromEncoding, - toEncoding, - ); -} +export const convert: TailorIconvAPI["convert"] = (...args) => api().convert(...args); /** - * Convert a buffer between encodings. - * @param buffer - Input bytes to convert - * @param fromEncoding - Source encoding name - * @param toEncoding - Target encoding name + * See {@link TailorIconvAPI.convertBuffer}. + * @param args - Forwarded to {@link TailorIconvAPI.convertBuffer} * @returns `string` when `toEncoding` is `"UTF8"` or `"UTF-8"`, otherwise `Uint8Array`. */ -export function convertBuffer( - buffer: Uint8Array | ArrayBuffer, - fromEncoding: string, - toEncoding: T, -): T extends "UTF8" | "UTF-8" ? string : Uint8Array { - return (globalThis as { tailor: { iconv: TailorIconvAPI } }).tailor.iconv.convertBuffer( - buffer, - fromEncoding, - toEncoding, - ); -} +export const convertBuffer: TailorIconvAPI["convertBuffer"] = (...args) => + api().convertBuffer(...args); /** - * Decode a buffer to a UTF-8 string using the given source encoding. - * @param buffer - Input bytes - * @param encoding - Source encoding name + * See {@link TailorIconvAPI.decode}. + * @param args - Forwarded to {@link TailorIconvAPI.decode} * @returns Decoded UTF-8 string */ -export function decode(buffer: Uint8Array | ArrayBuffer, encoding: string): string { - return (globalThis as { tailor: { iconv: TailorIconvAPI } }).tailor.iconv.decode( - buffer, - encoding, - ); -} +export const decode: TailorIconvAPI["decode"] = (...args) => api().decode(...args); /** - * Encode a UTF-8 string into the given target encoding. - * @param str - Input string - * @param encoding - Target encoding name + * See {@link TailorIconvAPI.encode}. + * @param args - Forwarded to {@link TailorIconvAPI.encode} * @returns `string` when `encoding` is `"UTF8"` or `"UTF-8"`, otherwise `Uint8Array`. */ -export function encode( - str: string, - encoding: T, -): T extends "UTF8" | "UTF-8" ? string : Uint8Array { - return (globalThis as { tailor: { iconv: TailorIconvAPI } }).tailor.iconv.encode(str, encoding); -} +export const encode: TailorIconvAPI["encode"] = (...args) => api().encode(...args); /** - * Returns the list of supported encoding names. + * See {@link TailorIconvAPI.encodings}. * @returns Array of encoding names supported by the platform iconv runtime */ -export function encodings(): string[] { - return (globalThis as { tailor: { iconv: TailorIconvAPI } }).tailor.iconv.encodings(); -} +export const encodings: TailorIconvAPI["encodings"] = () => api().encodings(); /** * Stateful converter for repeated conversions between a fixed encoding pair. @@ -133,10 +139,7 @@ export class Iconv { private impl: IconvInstance; constructor(fromEncoding: string, toEncoding: string) { - this.impl = new (globalThis as { tailor: { iconv: TailorIconvAPI } }).tailor.iconv.Iconv( - fromEncoding, - toEncoding, - ); + this.impl = new (api().Iconv)(fromEncoding, toEncoding); } /** diff --git a/packages/sdk/src/runtime/secretmanager.ts b/packages/sdk/src/runtime/secretmanager.ts index 92dee2252..c203d66eb 100644 --- a/packages/sdk/src/runtime/secretmanager.ts +++ b/packages/sdk/src/runtime/secretmanager.ts @@ -15,39 +15,46 @@ /** * Platform API surface for `tailor.secretmanager`. Describes the shape the * platform runtime injects on `globalThis.tailor.secretmanager`. - * @internal + * + * Each method below is also re-exported as a top-level named export from this + * module so callers can either `import * as secretmanager from + * "@tailor-platform/sdk/runtime/secretmanager"` or pick individual methods. */ export interface TailorSecretmanagerAPI { + /** + * Returns multiple secrets from a vault. Missing names are omitted from the result. + * @param vault - Vault name + * @param names - Secret names to fetch (use `as const` to narrow the result key) + * @returns Partial record keyed by the requested names + */ getSecrets( vault: string, names: T, ): Promise>>; + + /** + * Returns a single secret from a vault, or `undefined` when missing. + * @param vault - Vault name + * @param name - Secret name + * @returns The secret value, or `undefined` if not present + */ getSecret(vault: string, name: string): Promise; } +const api = (): TailorSecretmanagerAPI => + (globalThis as { tailor: { secretmanager: TailorSecretmanagerAPI } }).tailor.secretmanager; + /** - * Returns multiple secrets from a vault. Missing names are omitted from the result. - * @param vault - Vault name - * @param names - Secret names to fetch (use `as const` to narrow the result key) + * See {@link TailorSecretmanagerAPI.getSecrets}. + * @param args - Forwarded to {@link TailorSecretmanagerAPI.getSecrets} * @returns Partial record keyed by the requested names */ -export function getSecrets( - vault: string, - names: T, -): Promise>> { - return ( - globalThis as { tailor: { secretmanager: TailorSecretmanagerAPI } } - ).tailor.secretmanager.getSecrets(vault, names); -} +export const getSecrets: TailorSecretmanagerAPI["getSecrets"] = (...args) => + api().getSecrets(...args); /** - * Returns a single secret from a vault, or `undefined` when missing. - * @param vault - Vault name - * @param name - Secret name + * See {@link TailorSecretmanagerAPI.getSecret}. + * @param args - Forwarded to {@link TailorSecretmanagerAPI.getSecret} * @returns The secret value, or `undefined` if not present */ -export function getSecret(vault: string, name: string): Promise { - return ( - globalThis as { tailor: { secretmanager: TailorSecretmanagerAPI } } - ).tailor.secretmanager.getSecret(vault, name); -} +export const getSecret: TailorSecretmanagerAPI["getSecret"] = (...args) => api().getSecret(...args); diff --git a/packages/sdk/src/runtime/workflow.ts b/packages/sdk/src/runtime/workflow.ts index 37cec022c..70e266aab 100644 --- a/packages/sdk/src/runtime/workflow.ts +++ b/packages/sdk/src/runtime/workflow.ts @@ -32,80 +32,80 @@ export interface TriggerWorkflowOptions { /** * Platform API surface for `tailor.workflow`. Describes the shape the platform * runtime injects on `globalThis.tailor.workflow`. - * @internal + * + * Each method below is also re-exported as a top-level named export from this + * module so callers can either `import * as workflow from + * "@tailor-platform/sdk/runtime/workflow"` or pick individual methods. */ export interface TailorWorkflowAPI { + /** + * Triggers a workflow and returns its execution ID. + * @param workflowName - Workflow name as defined in tailor.config + * @param args - Arguments forwarded to the workflow's main job + * @param options - Optional trigger options (e.g. `authInvoker`) + * @returns The execution ID of the triggered workflow + */ triggerWorkflow( workflowName: string, args?: any, options?: TriggerWorkflowOptions, ): Promise; + + /** + * Triggers a job function and returns its result. + * @param jobName - Job name as defined in the workflow + * @param args - Arguments forwarded to the job + * @returns The job's return value + */ triggerJobFunction(jobName: string, args?: any): any; + + /** + * Suspends the current workflow execution and waits for an external signal to resume. + * @param key - Wait point key + * @param payload - Optional payload to record with the wait point + * @returns The payload supplied by the corresponding `resolve` call + */ wait(key: string, payload?: any): any; + + /** + * Resolves a waiting workflow execution, causing it to resume. + * @param executionId - The execution to resume + * @param key - Wait point key to resolve + * @param callback - Callback receiving the wait payload; its return value is forwarded to `wait` + * @returns A promise that resolves once the resolve has been recorded + */ resolve(executionId: string, key: string, callback: (waitPayload: any) => any): Promise; } +const api = (): TailorWorkflowAPI => + (globalThis as { tailor: { workflow: TailorWorkflowAPI } }).tailor.workflow; + /** - * Triggers a workflow and returns its execution ID. - * @param workflowName - Workflow name as defined in tailor.config - * @param args - Arguments forwarded to the workflow's main job - * @param options - Optional trigger options (e.g. `authInvoker`) + * See {@link TailorWorkflowAPI.triggerWorkflow}. + * @param args - Forwarded to {@link TailorWorkflowAPI.triggerWorkflow} * @returns The execution ID of the triggered workflow */ -export function triggerWorkflow( - workflowName: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - args?: any, - options?: TriggerWorkflowOptions, -): Promise { - return ( - globalThis as { tailor: { workflow: TailorWorkflowAPI } } - ).tailor.workflow.triggerWorkflow(workflowName, args, options); -} +export const triggerWorkflow: TailorWorkflowAPI["triggerWorkflow"] = (...args) => + api().triggerWorkflow(...args); /** - * Triggers a job function and returns its result. - * @param jobName - Job name as defined in the workflow - * @param args - Arguments forwarded to the job + * See {@link TailorWorkflowAPI.triggerJobFunction}. + * @param args - Forwarded to {@link TailorWorkflowAPI.triggerJobFunction} * @returns The job's return value */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function triggerJobFunction(jobName: string, args?: any): any { - return ( - globalThis as { tailor: { workflow: TailorWorkflowAPI } } - ).tailor.workflow.triggerJobFunction(jobName, args); -} +export const triggerJobFunction: TailorWorkflowAPI["triggerJobFunction"] = (...args) => + api().triggerJobFunction(...args); /** - * Suspends the current workflow execution and waits for an external signal to resume. - * @param key - Wait point key - * @param payload - Optional payload to record with the wait point + * See {@link TailorWorkflowAPI.wait}. + * @param args - Forwarded to {@link TailorWorkflowAPI.wait} * @returns The payload supplied by the corresponding `resolve` call */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function wait(key: string, payload?: any): any { - return (globalThis as { tailor: { workflow: TailorWorkflowAPI } }).tailor.workflow.wait( - key, - payload, - ); -} +export const wait: TailorWorkflowAPI["wait"] = (...args) => api().wait(...args); /** - * Resolves a waiting workflow execution, causing it to resume. - * @param executionId - The execution to resume - * @param key - Wait point key to resolve - * @param callback - Callback receiving the wait payload; its return value is forwarded to `wait` + * See {@link TailorWorkflowAPI.resolve}. + * @param args - Forwarded to {@link TailorWorkflowAPI.resolve} * @returns A promise that resolves once the resolve has been recorded */ -export function resolve( - executionId: string, - key: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - callback: (waitPayload: any) => any, -): Promise { - return (globalThis as { tailor: { workflow: TailorWorkflowAPI } }).tailor.workflow.resolve( - executionId, - key, - callback, - ); -} +export const resolve: TailorWorkflowAPI["resolve"] = (...args) => api().resolve(...args); From 90fc6ff0ceee1b6ba5f8df836f80fbf12db9139f Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 19 May 2026 22:13:12 +0900 Subject: [PATCH 30/35] refactor(sdk): reuse TailorWorkflowAPI in configure wait-point The wait-point module previously declared an inline shape for globalThis.tailor.workflow that duplicated TailorWorkflowAPI. Import the canonical type from @/runtime/workflow instead, and type the test file's globalThis cast with TailorRuntime as well. Configure module is allowed to import from @/runtime per the import boundary rules. --- .../configure/services/workflow/wait-point.test.ts | 3 ++- .../src/configure/services/workflow/wait-point.ts | 14 ++------------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/sdk/src/configure/services/workflow/wait-point.test.ts b/packages/sdk/src/configure/services/workflow/wait-point.test.ts index 0849ed065..4b8914af1 100644 --- a/packages/sdk/src/configure/services/workflow/wait-point.test.ts +++ b/packages/sdk/src/configure/services/workflow/wait-point.test.ts @@ -1,8 +1,9 @@ import { afterEach, describe, it, expect, expectTypeOf } from "vitest"; import { setupWaitPointMock, setupWorkflowMock } from "@/utils/test/mock"; import { defineWaitPoint, defineWaitPoints } from "./wait-point"; +import type { TailorRuntime } from "@/runtime"; -const TailorGlobal = globalThis as { tailor?: unknown }; +const TailorGlobal = globalThis as { tailor?: TailorRuntime }; describe("defineWaitPoints", () => { afterEach(() => { diff --git a/packages/sdk/src/configure/services/workflow/wait-point.ts b/packages/sdk/src/configure/services/workflow/wait-point.ts index 6e7c3a3d3..a2878ab67 100644 --- a/packages/sdk/src/configure/services/workflow/wait-point.ts +++ b/packages/sdk/src/configure/services/workflow/wait-point.ts @@ -1,4 +1,5 @@ import { brandValue } from "@/utils/brand"; +import type { TailorWorkflowAPI } from "@/runtime/workflow"; import type { JsonCompatible } from "@/types/helpers"; /** @@ -36,18 +37,7 @@ interface WaitPointWithSetter { } function getPlatformWorkflow() { - const platform = globalThis as { - tailor?: { - workflow?: { - wait: (k: string, p?: unknown) => unknown; - resolve: ( - e: string, - k: string, - c: (p: unknown) => unknown | Promise, - ) => Promise; - }; - }; - }; + const platform = globalThis as { tailor?: { workflow?: TailorWorkflowAPI } }; const workflow = platform.tailor?.workflow; if (!workflow) { throw new Error( From f5d3d38f0b0f5634d4ecd4cb108731f57adc2a57 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 19 May 2026 23:38:18 +0900 Subject: [PATCH 31/35] feat(sdk-codemod): add v2/tailordb-namespace and v2/drop-function-types-dep - v2/tailordb-namespace rewrites references to the deprecated capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, `typeof Tailordb.Client`) to the new lowercase `tailordb.*` namespace re-published by the SDK. - v2/drop-function-types-dep removes `@tailor-platform/function-types` from `package.json` dependency maps (`dependencies` / `devDependencies` / `peerDependencies` / `optionalDependencies`) and from `tsconfig.json` `compilerOptions.types`, since the declarations are now vendored inside the SDK. Also restore the capital-cased `Tailordb` ambient namespace as a `@deprecated` alias of the new lowercase `tailordb.*` namespace so existing code that uses `Tailordb.QueryResult` etc. continues to type-check against the vendored declarations, and revert `packages/sdk/src/cli/shared/mock.ts` to its main version which still uses `Tailordb.QueryResult`. --- .changeset/runtime-wrapper.md | 2 + .changeset/sdk-codemod-tailordb-namespace.md | 8 ++ .../v2/drop-function-types-dep/codemod.yaml | 7 + .../scripts/transform.ts | 131 ++++++++++++++++++ .../tests/basic-dependencies/expected.json | 8 ++ .../tests/basic-dependencies/input.json | 9 ++ .../tests/dev-dependency/expected.json | 3 + .../tests/dev-dependency/input.json | 6 + .../tests/multiple-locations/expected.json | 9 ++ .../tests/multiple-locations/input.json | 14 ++ .../tests/no-match/input.json | 7 + .../tests/tsconfig-types-array/expected.json | 12 ++ .../tests/tsconfig-types-array/input.json | 13 ++ .../expected.json | 3 + .../tsconfig-types-becomes-empty/input.json | 7 + .../v2/tailordb-namespace/codemod.yaml | 7 + .../tailordb-namespace/scripts/transform.ts | 36 +++++ .../tests/basic-query-result/expected.ts | 5 + .../tests/basic-query-result/input.ts | 5 + .../client-instance-and-typeof/expected.ts | 7 + .../tests/client-instance-and-typeof/input.ts | 7 + .../tests/generic-arg/expected.ts | 9 ++ .../tests/generic-arg/input.ts | 9 ++ .../tests/multiple-members/expected.ts | 9 ++ .../tests/multiple-members/input.ts | 9 ++ .../tests/no-match/input.ts | 8 ++ .../tests/unrelated-prefix/input.ts | 15 ++ packages/sdk-codemod/src/registry.ts | 21 +++ packages/sdk-codemod/src/transform.test.ts | 8 ++ packages/sdk-codemod/tsdown.config.ts | 4 + packages/sdk/src/cli/shared/mock.ts | 4 +- packages/sdk/src/runtime/globals.ts | 34 +++++ 32 files changed, 434 insertions(+), 2 deletions(-) create mode 100644 .changeset/sdk-codemod-tailordb-namespace.md create mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/basic-dependencies/expected.json create mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/basic-dependencies/input.json create mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/dev-dependency/expected.json create mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/dev-dependency/input.json create mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/multiple-locations/expected.json create mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/multiple-locations/input.json create mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/no-match/input.json create mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-array/expected.json create mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-array/input.json create mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-becomes-empty/expected.json create mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-becomes-empty/input.json create mode 100644 packages/sdk-codemod/codemods/v2/tailordb-namespace/codemod.yaml create mode 100644 packages/sdk-codemod/codemods/v2/tailordb-namespace/scripts/transform.ts create mode 100644 packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/basic-query-result/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/basic-query-result/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/client-instance-and-typeof/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/client-instance-and-typeof/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/generic-arg/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/generic-arg/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/multiple-members/expected.ts create mode 100644 packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/multiple-members/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/no-match/input.ts create mode 100644 packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/unrelated-prefix/input.ts diff --git a/.changeset/runtime-wrapper.md b/.changeset/runtime-wrapper.md index d172ca63a..2328bab6e 100644 --- a/.changeset/runtime-wrapper.md +++ b/.changeset/runtime-wrapper.md @@ -15,6 +15,8 @@ const { metadata } = await file.upload("ns", "Document", "attachment", recordId, The SDK no longer depends on the external `@tailor-platform/function-types` package; its declarations are now vendored inside the SDK. For backwards compatibility the ambient `tailor.*` / `tailordb.*` types are still activated automatically when you import from `@tailor-platform/sdk`, so existing code keeps type-checking with no changes. This implicit activation will be removed in v2.0 — new code is encouraged to use the typed wrappers from `@tailor-platform/sdk/runtime`, or to opt into the globals explicitly via `import "@tailor-platform/sdk/runtime/globals"` (or by listing the entry in `tsconfig.json`'s `compilerOptions.types`). +The capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`) is preserved as a `@deprecated` alias of the new lowercase `tailordb.*` namespace for source-level compatibility with `@tailor-platform/function-types`. It will be removed in v2.0; run `pnpm dlx @tailor-platform/sdk-codemod v2/tailordb-namespace` to migrate, and `pnpm dlx @tailor-platform/sdk-codemod v2/drop-function-types-dep` to drop the now-redundant dependency entry from `package.json` and the corresponding `tsconfig.json` `compilerOptions.types` entry. + Other test-mock changes from `@tailor-platform/sdk/vitest`: - Breaking: when an `openDownloadStream` (or `toFileStream()`) call consumes a queued mock result, raw `Uint8Array` / `ArrayBuffer` payloads are now rejected. Enqueue a structured iterable of `StreamValue` items (`{ type: "metadata" }`, `{ type: "chunk", data, position }`, `{ type: "complete" }`) so test streams stay aligned with the platform's structured stream contract. The shorthand `Uint8Array` enqueue is still accepted by `download` / `downloadAsBase64`. diff --git a/.changeset/sdk-codemod-tailordb-namespace.md b/.changeset/sdk-codemod-tailordb-namespace.md new file mode 100644 index 000000000..0d01a4ab7 --- /dev/null +++ b/.changeset/sdk-codemod-tailordb-namespace.md @@ -0,0 +1,8 @@ +--- +"@tailor-platform/sdk-codemod": patch +--- + +Add two v2 codemods for the `@tailor-platform/function-types` → `@tailor-platform/sdk` vendoring: + +- `v2/tailordb-namespace`: rewrite references to the deprecated capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, `typeof Tailordb.Client`) to the new lowercase `tailordb.*` namespace re-published by the SDK. +- `v2/drop-function-types-dep`: remove `@tailor-platform/function-types` from `package.json` (`dependencies` / `devDependencies` / `peerDependencies` / `optionalDependencies`) and from `tsconfig.json` `compilerOptions.types`, since its declarations are now vendored inside the SDK. diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/codemod.yaml b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/codemod.yaml new file mode 100644 index 000000000..13913bb64 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/drop-function-types-dep" +version: "1.0.0" +description: "Drop the `@tailor-platform/function-types` dependency and tsconfig types entry; its declarations are now vendored inside `@tailor-platform/sdk`." +engine: jssg +language: text +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/scripts/transform.ts new file mode 100644 index 000000000..63f7a0cc7 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/scripts/transform.ts @@ -0,0 +1,131 @@ +import * as path from "pathe"; + +const PACKAGE_NAME = "@tailor-platform/function-types"; + +// `dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies`, +// and `bundleDependencies` (the standard alias `bundledDependencies` is also +// covered) — every place npm/pnpm/yarn recognize as a dependency mapping. +const DEPENDENCY_FIELDS = [ + "dependencies", + "devDependencies", + "peerDependencies", + "optionalDependencies", + "bundleDependencies", + "bundledDependencies", +] as const; + +function removeFromDependencyMap(parsed: Record, field: string): boolean { + const map = parsed[field]; + if (typeof map !== "object" || map == null || Array.isArray(map)) return false; + const record = map as Record; + if (!(PACKAGE_NAME in record)) return false; + delete record[PACKAGE_NAME]; + // Drop the dependency section entirely when removing this key empties it, + // so re-running the codemod (or pnpm install) does not leave behind a noisy + // `"dependencies": {}` block. + if (Object.keys(record).length === 0) { + delete parsed[field]; + } + return true; +} + +function removeFromCompilerTypes(parsed: Record): boolean { + const compilerOptions = parsed.compilerOptions; + if (typeof compilerOptions !== "object" || compilerOptions == null) return false; + const opts = compilerOptions as Record; + const types = opts.types; + if (!Array.isArray(types)) return false; + const filtered = types.filter((t) => t !== PACKAGE_NAME); + if (filtered.length === types.length) return false; + if (filtered.length === 0) { + delete opts.types; + } else { + opts.types = filtered; + } + return true; +} + +function tryParseJson(source: string): Record | null { + try { + return JSON.parse(source) as Record; + } catch { + return null; + } +} + +function stripJsonComments(source: string): string { + // String-aware best-effort comment stripper for tsconfig.json. JSON.parse + // rejects `//` and `/* */` comments but `tsc` accepts them. Only invoked as + // a fallback after vanilla `JSON.parse` fails, so plain JSON paths skip + // this entirely. + let out = ""; + let i = 0; + while (i < source.length) { + const ch = source[i]!; + if (ch === '"') { + const start = i; + i++; + while (i < source.length) { + const c = source[i]!; + if (c === "\\" && i + 1 < source.length) { + i += 2; + continue; + } + if (c === '"') { + i++; + break; + } + i++; + } + out += source.slice(start, i); + continue; + } + if (ch === "/" && source[i + 1] === "/") { + while (i < source.length && source[i] !== "\n") i++; + continue; + } + if (ch === "/" && source[i + 1] === "*") { + i += 2; + while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i++; + i += 2; + continue; + } + out += ch; + i++; + } + return out; +} + +/** + * Remove the `@tailor-platform/function-types` reference from both + * `package.json` dependency maps and `tsconfig.json` + * `compilerOptions.types`. The package's declarations are now vendored + * inside `@tailor-platform/sdk` and activated automatically when importing + * the SDK (or explicitly via `import "@tailor-platform/sdk/runtime/globals"`), + * so the external dependency is no longer needed. + * + * The codemod dispatches purely on JSON content shape, so it works regardless + * of the fixture file name during tests. Runner-side file selection should be + * narrowed via the registry's `filePatterns` (`package.json` and + * `tsconfig*.json`). + * @param source - File contents + * @param filePath - Absolute path to the file (used to ignore non-JSON inputs) + * @returns Transformed source or null when nothing matched. + */ +export default function transform(source: string, filePath: string): string | null { + if (!source.includes(PACKAGE_NAME)) return null; + if (path.extname(filePath).toLowerCase() !== ".json") return null; + + const parsed = tryParseJson(source) ?? tryParseJson(stripJsonComments(source)); + if (!parsed) return null; + + let modified = false; + for (const field of DEPENDENCY_FIELDS) { + if (removeFromDependencyMap(parsed, field)) modified = true; + } + if (removeFromCompilerTypes(parsed)) modified = true; + + if (!modified) return null; + const trailing = source.endsWith("\n") ? "\n" : ""; + return JSON.stringify(parsed, null, 2) + trailing; +} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/basic-dependencies/expected.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/basic-dependencies/expected.json new file mode 100644 index 000000000..415ce3f12 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/basic-dependencies/expected.json @@ -0,0 +1,8 @@ +{ + "name": "my-app", + "version": "0.1.0", + "dependencies": { + "@tailor-platform/sdk": "1.48.0", + "kysely": "0.28.0" + } +} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/basic-dependencies/input.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/basic-dependencies/input.json new file mode 100644 index 000000000..5b05334c8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/basic-dependencies/input.json @@ -0,0 +1,9 @@ +{ + "name": "my-app", + "version": "0.1.0", + "dependencies": { + "@tailor-platform/function-types": "0.8.5", + "@tailor-platform/sdk": "1.48.0", + "kysely": "0.28.0" + } +} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/dev-dependency/expected.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/dev-dependency/expected.json new file mode 100644 index 000000000..5c71cdb7d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/dev-dependency/expected.json @@ -0,0 +1,3 @@ +{ + "name": "my-lib" +} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/dev-dependency/input.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/dev-dependency/input.json new file mode 100644 index 000000000..15cc747d1 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/dev-dependency/input.json @@ -0,0 +1,6 @@ +{ + "name": "my-lib", + "devDependencies": { + "@tailor-platform/function-types": "0.8.5" + } +} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/multiple-locations/expected.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/multiple-locations/expected.json new file mode 100644 index 000000000..72ee94f2d --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/multiple-locations/expected.json @@ -0,0 +1,9 @@ +{ + "name": "my-monorepo-pkg", + "dependencies": { + "@tailor-platform/sdk": "1.48.0" + }, + "devDependencies": { + "typescript": "5.9.2" + } +} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/multiple-locations/input.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/multiple-locations/input.json new file mode 100644 index 000000000..e8251b66f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/multiple-locations/input.json @@ -0,0 +1,14 @@ +{ + "name": "my-monorepo-pkg", + "dependencies": { + "@tailor-platform/function-types": "0.8.5", + "@tailor-platform/sdk": "1.48.0" + }, + "devDependencies": { + "@tailor-platform/function-types": "0.8.5", + "typescript": "5.9.2" + }, + "peerDependencies": { + "@tailor-platform/function-types": "*" + } +} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/no-match/input.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/no-match/input.json new file mode 100644 index 000000000..275f43dcf --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/no-match/input.json @@ -0,0 +1,7 @@ +{ + "name": "my-app", + "dependencies": { + "@tailor-platform/sdk": "1.48.0", + "kysely": "0.28.0" + } +} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-array/expected.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-array/expected.json new file mode 100644 index 000000000..1e34df4cc --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-array/expected.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "types": [ + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-array/input.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-array/input.json new file mode 100644 index 000000000..78ed14364 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-array/input.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "types": [ + "node", + "@tailor-platform/function-types" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-becomes-empty/expected.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-becomes-empty/expected.json new file mode 100644 index 000000000..875cb6001 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-becomes-empty/expected.json @@ -0,0 +1,3 @@ +{ + "compilerOptions": {} +} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-becomes-empty/input.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-becomes-empty/input.json new file mode 100644 index 000000000..d2fe46dea --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-becomes-empty/input.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "types": [ + "@tailor-platform/function-types" + ] + } +} diff --git a/packages/sdk-codemod/codemods/v2/tailordb-namespace/codemod.yaml b/packages/sdk-codemod/codemods/v2/tailordb-namespace/codemod.yaml new file mode 100644 index 000000000..83219b1f8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/tailordb-namespace/codemod.yaml @@ -0,0 +1,7 @@ +name: "@tailor-platform/tailordb-namespace" +version: "1.0.0" +description: "Rename the deprecated capital-cased `Tailordb` ambient namespace to lowercase `tailordb` (e.g. `Tailordb.QueryResult` → `tailordb.QueryResult`)" +engine: jssg +language: typescript +since: "1.0.0" +until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/tailordb-namespace/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/tailordb-namespace/scripts/transform.ts new file mode 100644 index 000000000..a803bbea5 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/tailordb-namespace/scripts/transform.ts @@ -0,0 +1,36 @@ +// Members exposed by the deprecated `Tailordb` ambient namespace from +// `@tailor-platform/function-types`. Each was a type-only declaration that +// has been re-published under the new lowercase `tailordb` namespace by the +// SDK. Anything outside this list is left untouched so user-defined symbols +// that happen to share the `Tailordb.` prefix are not rewritten by accident. +const TAILORDB_MEMBERS = ["QueryResult", "CommandType", "Client"] as const; + +const MEMBER_GROUP = TAILORDB_MEMBERS.join("|"); + +// Match `Tailordb.` with word boundaries so neither prefix nor +// suffix collisions (e.g. `MyTailordb.X`, `Tailordb.QueryResultExtra`) are +// rewritten. Generic-argument lists and `typeof` qualifiers are not part of +// the match — they fall outside the boundary and are preserved verbatim. +const PATTERN = new RegExp(String.raw`\bTailordb\.(${MEMBER_GROUP})\b`, "g"); + +/** + * Rewrite references to the deprecated capital-cased `Tailordb` ambient + * namespace to the new lowercase `tailordb` namespace. The capital-cased + * namespace was inherited from `@tailor-platform/function-types`; the SDK + * keeps it as a `@deprecated` alias in v1 and removes it in v2. + * + * Only the known type-only members (`QueryResult`, `CommandType`, `Client`) + * are rewritten so that unrelated user-defined symbols sharing the + * `Tailordb.` prefix remain untouched. Both type-position references + * (`Tailordb.QueryResult`, `typeof Tailordb.Client`) and value-position + * references (`new Tailordb.Client(...)`) are handled by the same rule. + * @param source - File contents + * @param _filePath - Absolute path to the file (kept for the runner signature) + * @returns Transformed source or null when nothing matched. + */ +export default function transform(source: string, _filePath: string): string | null { + if (!source.includes("Tailordb.")) return null; + PATTERN.lastIndex = 0; + const updated = source.replace(PATTERN, (_match, member: string) => `tailordb.${member}`); + return updated === source ? null : updated; +} diff --git a/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/basic-query-result/expected.ts b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/basic-query-result/expected.ts new file mode 100644 index 000000000..f7f20316a --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/basic-query-result/expected.ts @@ -0,0 +1,5 @@ +async function fetchRows(): Promise> { + return {} as tailordb.QueryResult; +} + +const command: tailordb.CommandType = "SELECT"; diff --git a/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/basic-query-result/input.ts b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/basic-query-result/input.ts new file mode 100644 index 000000000..7ced83968 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/basic-query-result/input.ts @@ -0,0 +1,5 @@ +async function fetchRows(): Promise> { + return {} as Tailordb.QueryResult; +} + +const command: Tailordb.CommandType = "SELECT"; diff --git a/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/client-instance-and-typeof/expected.ts b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/client-instance-and-typeof/expected.ts new file mode 100644 index 000000000..c0374a437 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/client-instance-and-typeof/expected.ts @@ -0,0 +1,7 @@ +function takeClient(client: tailordb.Client): void { + void client; +} + +function makeClient(Ctor: typeof tailordb.Client): tailordb.Client { + return new Ctor({ namespace: "demo" }); +} diff --git a/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/client-instance-and-typeof/input.ts b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/client-instance-and-typeof/input.ts new file mode 100644 index 000000000..d9c1d4f62 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/client-instance-and-typeof/input.ts @@ -0,0 +1,7 @@ +function takeClient(client: Tailordb.Client): void { + void client; +} + +function makeClient(Ctor: typeof Tailordb.Client): Tailordb.Client { + return new Ctor({ namespace: "demo" }); +} diff --git a/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/generic-arg/expected.ts b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/generic-arg/expected.ts new file mode 100644 index 000000000..1648242a8 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/generic-arg/expected.ts @@ -0,0 +1,9 @@ +interface Row { + id: string; +} + +type RowsResult = tailordb.QueryResult; + +function describe(result: tailordb.QueryResult<{ id: number; tags: readonly string[] }>): number { + return result.rowCount; +} diff --git a/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/generic-arg/input.ts b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/generic-arg/input.ts new file mode 100644 index 000000000..755d627f3 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/generic-arg/input.ts @@ -0,0 +1,9 @@ +interface Row { + id: string; +} + +type RowsResult = Tailordb.QueryResult; + +function describe(result: Tailordb.QueryResult<{ id: number; tags: readonly string[] }>): number { + return result.rowCount; +} diff --git a/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/multiple-members/expected.ts b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/multiple-members/expected.ts new file mode 100644 index 000000000..47d38d488 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/multiple-members/expected.ts @@ -0,0 +1,9 @@ +async function run(): Promise> { + const cmd: tailordb.CommandType = "SELECT"; + const client: tailordb.Client = new (tailordb.Client as typeof tailordb.Client)({ + namespace: "demo", + }); + void cmd; + void client; + return {} as tailordb.QueryResult; +} diff --git a/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/multiple-members/input.ts b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/multiple-members/input.ts new file mode 100644 index 000000000..ac0d7fd2c --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/multiple-members/input.ts @@ -0,0 +1,9 @@ +async function run(): Promise> { + const cmd: Tailordb.CommandType = "SELECT"; + const client: Tailordb.Client = new (Tailordb.Client as typeof Tailordb.Client)({ + namespace: "demo", + }); + void cmd; + void client; + return {} as Tailordb.QueryResult; +} diff --git a/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/no-match/input.ts b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/no-match/input.ts new file mode 100644 index 000000000..2b920d01f --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/no-match/input.ts @@ -0,0 +1,8 @@ +import { createResolver, t } from "@tailor-platform/sdk"; + +export default createResolver({ + name: "hello", + type: "Query", + output: t.string(), + body: () => "world", +}); diff --git a/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/unrelated-prefix/input.ts b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/unrelated-prefix/input.ts new file mode 100644 index 000000000..915f92899 --- /dev/null +++ b/packages/sdk-codemod/codemods/v2/tailordb-namespace/tests/unrelated-prefix/input.ts @@ -0,0 +1,15 @@ +// Identifiers that merely *start* with `Tailordb` (or have an unknown +// member) must not be rewritten by the codemod. + +namespace MyTailordb { + export type QueryResult = { rows: T[] }; +} + +type Mine = MyTailordb.QueryResult<{ id: string }>; + +// Unknown member on the deprecated namespace stays untouched (best-effort +// safety net — there is no `Tailordb.NotAType` on the SDK side). +type Untouched = Tailordb.NotAType; + +// A suffix-extended member name must also stay intact. +type Extra = Tailordb.QueryResultExtra; diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index ddd4bf315..95121f7ac 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -77,6 +77,27 @@ const allCodemods: CodemodPackage[] = [ scriptPath: "v2/auth-invoker-unwrap/scripts/transform.js", legacyPatterns: ["auth.invoker"], }, + { + id: "v2/tailordb-namespace", + name: "Tailordb → tailordb (lowercase ambient namespace)", + description: + "Rewrite references to the deprecated capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, `typeof Tailordb.Client`) to the new lowercase `tailordb.*` namespace re-published by the SDK in place of `@tailor-platform/function-types`.", + since: "1.0.0", + until: "2.0.0", + scriptPath: "v2/tailordb-namespace/scripts/transform.js", + legacyPatterns: ["Tailordb."], + }, + { + id: "v2/drop-function-types-dep", + name: "Drop @tailor-platform/function-types dependency", + description: + "Remove `@tailor-platform/function-types` from package.json (`dependencies` / `devDependencies` / `peerDependencies` / `optionalDependencies`) and from `tsconfig.json` `compilerOptions.types`. Its declarations are now vendored inside `@tailor-platform/sdk` and activated automatically.", + since: "1.0.0", + until: "2.0.0", + scriptPath: "v2/drop-function-types-dep/scripts/transform.js", + filePatterns: ["**/package.json", "**/tsconfig*.json"], + legacyPatterns: ["@tailor-platform/function-types"], + }, ]; /** diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index cef6eb1ac..c48082c85 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -90,4 +90,12 @@ describe("codemod transforms", () => { it("v2/auth-invoker-unwrap transforms correctly", async () => { await runFixtureCases("v2/auth-invoker-unwrap"); }); + + it("v2/tailordb-namespace transforms correctly", async () => { + await runFixtureCases("v2/tailordb-namespace"); + }); + + it("v2/drop-function-types-dep transforms correctly", async () => { + await runFixtureCases("v2/drop-function-types-dep"); + }); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index c6c799ed5..245832b7b 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -25,6 +25,10 @@ export default defineConfig([ "v2/cli-rename/scripts/transform": "codemods/v2/cli-rename/scripts/transform.ts", "v2/auth-invoker-unwrap/scripts/transform": "codemods/v2/auth-invoker-unwrap/scripts/transform.ts", + "v2/tailordb-namespace/scripts/transform": + "codemods/v2/tailordb-namespace/scripts/transform.ts", + "v2/drop-function-types-dep/scripts/transform": + "codemods/v2/drop-function-types-dep/scripts/transform.ts", }, format: ["esm"], target: "node18", diff --git a/packages/sdk/src/cli/shared/mock.ts b/packages/sdk/src/cli/shared/mock.ts index dd6371938..db50feaa1 100644 --- a/packages/sdk/src/cli/shared/mock.ts +++ b/packages/sdk/src/cli/shared/mock.ts @@ -10,8 +10,8 @@ constructor(_config: { namespace: string }) {} async connect(): Promise {} async end(): Promise {} - async queryObject(): Promise> { - return {} as Promise>; + async queryObject(): Promise> { + return {} as Promise>; } }, }; diff --git a/packages/sdk/src/runtime/globals.ts b/packages/sdk/src/runtime/globals.ts index 48168a0c1..925821b2e 100644 --- a/packages/sdk/src/runtime/globals.ts +++ b/packages/sdk/src/runtime/globals.ts @@ -52,6 +52,40 @@ declare global { // eslint-disable-next-line no-var var tailordb: TailordbRuntime; + /** + * @deprecated Use the lowercase `tailordb.*` namespace instead (e.g. + * `tailordb.QueryResult`, `tailordb.CommandType`, + * `typeof tailordb.Client`). This capital-cased namespace is retained + * only for backwards compatibility with `@tailor-platform/function-types` + * and will be removed in v2. Run + * `pnpm dlx @tailor-platform/sdk-codemod v2/tailordb-namespace` + * to migrate. + */ + namespace Tailordb { + /** + * @deprecated Use `tailordb.Client` (lowercase) instead. + * Will be removed in v2. + */ + class Client { + constructor(config: { namespace: string }); + connect(): Promise; + end(): Promise; + queryObject(sql: string, args?: readonly unknown[]): Promise>; + } + + /** + * @deprecated Use `tailordb.QueryResult` (lowercase) instead. + * Will be removed in v2. + */ + type QueryResult = TailordbQueryResult; + + /** + * @deprecated Use `tailordb.CommandType` (lowercase) instead. + * Will be removed in v2. + */ + type CommandType = TailordbCommandType; + } + namespace tailor { namespace idp { type ClientConfig = IdpClientConfig; From 429cdf7ca131b5c37725609a4f750ccf63fd97e3 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Tue, 19 May 2026 23:52:33 +0900 Subject: [PATCH 32/35] revert(sdk): move vitest tests back under __tests__/ The vitest test colocation will be handled in a separate PR. This restores the `__tests__/` layout and the corresponding `vitest.config.ts` include / exclude patterns and `knip.json` ignore list so this PR no longer mixes the colocation refactor with the function-types vendoring work. --- packages/sdk/knip.json | 2 -- .../sdk/src/vitest/{ => __tests__}/blocked-modules.test.ts | 2 +- packages/sdk/src/vitest/{ => __tests__}/index.test.ts | 2 +- packages/sdk/src/vitest/{ => __tests__}/integration.test.ts | 2 +- .../integration/fixtures/uses-node-crypto-types.ts | 0 .../integration/fixtures/uses-node-crypto.ts | 0 .../{ => __tests__}/integration/fixtures/uses-web-crypto.ts | 0 .../vitest/{ => __tests__}/integration/should-fail.test.ts | 0 .../vitest/{ => __tests__}/integration/should-pass.test.ts | 0 .../src/vitest/{ => __tests__}/integration/vitest.config.ts | 6 +++--- packages/sdk/src/vitest/{ => __tests__}/mock-types.test.ts | 2 +- packages/sdk/src/vitest/{ => __tests__}/mock.test.ts | 2 +- packages/sdk/src/vitest/{ => __tests__}/plugin.test.ts | 2 +- packages/sdk/src/vitest/{ => __tests__}/setup.test.ts | 2 +- packages/sdk/vitest.config.ts | 5 +++-- 15 files changed, 13 insertions(+), 14 deletions(-) rename packages/sdk/src/vitest/{ => __tests__}/blocked-modules.test.ts (97%) rename packages/sdk/src/vitest/{ => __tests__}/index.test.ts (97%) rename packages/sdk/src/vitest/{ => __tests__}/integration.test.ts (98%) rename packages/sdk/src/vitest/{ => __tests__}/integration/fixtures/uses-node-crypto-types.ts (100%) rename packages/sdk/src/vitest/{ => __tests__}/integration/fixtures/uses-node-crypto.ts (100%) rename packages/sdk/src/vitest/{ => __tests__}/integration/fixtures/uses-web-crypto.ts (100%) rename packages/sdk/src/vitest/{ => __tests__}/integration/should-fail.test.ts (100%) rename packages/sdk/src/vitest/{ => __tests__}/integration/should-pass.test.ts (100%) rename packages/sdk/src/vitest/{ => __tests__}/integration/vitest.config.ts (68%) rename packages/sdk/src/vitest/{ => __tests__}/mock-types.test.ts (96%) rename packages/sdk/src/vitest/{ => __tests__}/mock.test.ts (99%) rename packages/sdk/src/vitest/{ => __tests__}/plugin.test.ts (99%) rename packages/sdk/src/vitest/{ => __tests__}/setup.test.ts (99%) diff --git a/packages/sdk/knip.json b/packages/sdk/knip.json index a89433445..5d54abc18 100644 --- a/packages/sdk/knip.json +++ b/packages/sdk/knip.json @@ -5,10 +5,8 @@ "ignore": [ "scripts/**", "e2e/fixtures/**", - "eslint-rules/__tests__/fixtures/**", "src/cli/commands/deploy/__test_fixtures__/**", "src/types/*.ts", - "src/vitest/integration/vitest.config.ts", "zinfer.config.ts" ], "ignoreDependencies": [ diff --git a/packages/sdk/src/vitest/blocked-modules.test.ts b/packages/sdk/src/vitest/__tests__/blocked-modules.test.ts similarity index 97% rename from packages/sdk/src/vitest/blocked-modules.test.ts rename to packages/sdk/src/vitest/__tests__/blocked-modules.test.ts index 622ca108d..7bb3ea406 100644 --- a/packages/sdk/src/vitest/blocked-modules.test.ts +++ b/packages/sdk/src/vitest/__tests__/blocked-modules.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { getBlockedMessage, isBlockedModule } from "./blocked-modules"; +import { getBlockedMessage, isBlockedModule } from "../blocked-modules"; describe("isBlockedModule", () => { test("recognizes node:-prefixed builtins", () => { diff --git a/packages/sdk/src/vitest/index.test.ts b/packages/sdk/src/vitest/__tests__/index.test.ts similarity index 97% rename from packages/sdk/src/vitest/index.test.ts rename to packages/sdk/src/vitest/__tests__/index.test.ts index 496970ac9..6330780b4 100644 --- a/packages/sdk/src/vitest/index.test.ts +++ b/packages/sdk/src/vitest/__tests__/index.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { isAbsolute } from "node:path"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import { tailorRuntime } from "./index"; +import { tailorRuntime } from "../index"; describe("tailorRuntime", () => { const ENV_VAR = "__TAILOR_RUNTIME_CONFIG"; diff --git a/packages/sdk/src/vitest/integration.test.ts b/packages/sdk/src/vitest/__tests__/integration.test.ts similarity index 98% rename from packages/sdk/src/vitest/integration.test.ts rename to packages/sdk/src/vitest/__tests__/integration.test.ts index c24651495..21657be7a 100644 --- a/packages/sdk/src/vitest/integration.test.ts +++ b/packages/sdk/src/vitest/__tests__/integration.test.ts @@ -12,7 +12,7 @@ const configPath = resolve(integrationDir, "vitest.config.ts"); // Run the nested `vitest run` from the SDK package root (not src/) so the // subprocess sees the package's `package.json` and `tsconfig.json` for // module resolution and TS transforms. -const sdkDir = resolve(currentDir, "../.."); +const sdkDir = resolve(currentDir, "../../.."); // Resolve the workspace's installed Vitest entry rather than relying on `npx`, // which may perform online package resolution and slow down / destabilize CI. diff --git a/packages/sdk/src/vitest/integration/fixtures/uses-node-crypto-types.ts b/packages/sdk/src/vitest/__tests__/integration/fixtures/uses-node-crypto-types.ts similarity index 100% rename from packages/sdk/src/vitest/integration/fixtures/uses-node-crypto-types.ts rename to packages/sdk/src/vitest/__tests__/integration/fixtures/uses-node-crypto-types.ts diff --git a/packages/sdk/src/vitest/integration/fixtures/uses-node-crypto.ts b/packages/sdk/src/vitest/__tests__/integration/fixtures/uses-node-crypto.ts similarity index 100% rename from packages/sdk/src/vitest/integration/fixtures/uses-node-crypto.ts rename to packages/sdk/src/vitest/__tests__/integration/fixtures/uses-node-crypto.ts diff --git a/packages/sdk/src/vitest/integration/fixtures/uses-web-crypto.ts b/packages/sdk/src/vitest/__tests__/integration/fixtures/uses-web-crypto.ts similarity index 100% rename from packages/sdk/src/vitest/integration/fixtures/uses-web-crypto.ts rename to packages/sdk/src/vitest/__tests__/integration/fixtures/uses-web-crypto.ts diff --git a/packages/sdk/src/vitest/integration/should-fail.test.ts b/packages/sdk/src/vitest/__tests__/integration/should-fail.test.ts similarity index 100% rename from packages/sdk/src/vitest/integration/should-fail.test.ts rename to packages/sdk/src/vitest/__tests__/integration/should-fail.test.ts diff --git a/packages/sdk/src/vitest/integration/should-pass.test.ts b/packages/sdk/src/vitest/__tests__/integration/should-pass.test.ts similarity index 100% rename from packages/sdk/src/vitest/integration/should-pass.test.ts rename to packages/sdk/src/vitest/__tests__/integration/should-pass.test.ts diff --git a/packages/sdk/src/vitest/integration/vitest.config.ts b/packages/sdk/src/vitest/__tests__/integration/vitest.config.ts similarity index 68% rename from packages/sdk/src/vitest/integration/vitest.config.ts rename to packages/sdk/src/vitest/__tests__/integration/vitest.config.ts index 0168b6222..364eb0ff3 100644 --- a/packages/sdk/src/vitest/integration/vitest.config.ts +++ b/packages/sdk/src/vitest/__tests__/integration/vitest.config.ts @@ -1,7 +1,7 @@ import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; -import { createBlockPlugin } from "../plugin"; +import { createBlockPlugin } from "../../plugin"; const here = dirname(fileURLToPath(import.meta.url)); @@ -9,8 +9,8 @@ export default defineConfig({ plugins: [createBlockPlugin()], test: { watch: false, - environment: resolve(here, "../environment.ts"), - setupFiles: [resolve(here, "../setup.ts")], + environment: resolve(here, "../../environment.ts"), + setupFiles: [resolve(here, "../../setup.ts")], include: ["./**/*.test.ts"], root: here, }, diff --git a/packages/sdk/src/vitest/mock-types.test.ts b/packages/sdk/src/vitest/__tests__/mock-types.test.ts similarity index 96% rename from packages/sdk/src/vitest/mock-types.test.ts rename to packages/sdk/src/vitest/__tests__/mock-types.test.ts index ce6c1778b..05f982bab 100644 --- a/packages/sdk/src/vitest/mock-types.test.ts +++ b/packages/sdk/src/vitest/__tests__/mock-types.test.ts @@ -8,7 +8,7 @@ */ import "@/runtime/globals"; import { afterAll, beforeAll, describe, expectTypeOf, test } from "vitest"; -import { injectMocks, cleanupMocks } from "./mock"; +import { injectMocks, cleanupMocks } from "../mock"; beforeAll(() => injectMocks(globalThis)); afterAll(() => cleanupMocks(globalThis)); diff --git a/packages/sdk/src/vitest/mock.test.ts b/packages/sdk/src/vitest/__tests__/mock.test.ts similarity index 99% rename from packages/sdk/src/vitest/mock.test.ts rename to packages/sdk/src/vitest/__tests__/mock.test.ts index b53b0c943..8af410579 100644 --- a/packages/sdk/src/vitest/mock.test.ts +++ b/packages/sdk/src/vitest/__tests__/mock.test.ts @@ -12,7 +12,7 @@ import { cleanupMocks, STATE_KEY, RUNTIME_FLAG_KEY, -} from "./mock"; +} from "../mock"; describe("mock", () => { beforeEach(() => { diff --git a/packages/sdk/src/vitest/plugin.test.ts b/packages/sdk/src/vitest/__tests__/plugin.test.ts similarity index 99% rename from packages/sdk/src/vitest/plugin.test.ts rename to packages/sdk/src/vitest/__tests__/plugin.test.ts index fc67a4699..1ff0cbe57 100644 --- a/packages/sdk/src/vitest/plugin.test.ts +++ b/packages/sdk/src/vitest/__tests__/plugin.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { isAbsolute } from "node:path"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import { createBlockPlugin, createEnvironmentPlugin } from "./plugin"; +import { createBlockPlugin, createEnvironmentPlugin } from "../plugin"; type ImportNode = { type: "ImportDeclaration" | "ExportNamedDeclaration" | "ExportAllDeclaration"; diff --git a/packages/sdk/src/vitest/setup.test.ts b/packages/sdk/src/vitest/__tests__/setup.test.ts similarity index 99% rename from packages/sdk/src/vitest/setup.test.ts rename to packages/sdk/src/vitest/__tests__/setup.test.ts index 4c5367bf6..55c1cda5f 100644 --- a/packages/sdk/src/vitest/setup.test.ts +++ b/packages/sdk/src/vitest/__tests__/setup.test.ts @@ -8,7 +8,7 @@ import { loadSecretsFromConfig, removeBlockedGlobals, restoreBlockedGlobals, -} from "./setup"; +} from "../setup"; describe("extractVaultStore", () => { test("unwraps a defineSecretManager() shape via the .vaults field", () => { diff --git a/packages/sdk/vitest.config.ts b/packages/sdk/vitest.config.ts index 3d681b437..14d2503df 100644 --- a/packages/sdk/vitest.config.ts +++ b/packages/sdk/vitest.config.ts @@ -16,14 +16,15 @@ export default defineConfig({ extends: true, test: { name: "unit", - include: ["**/?(*.)+(spec|test).ts"], + include: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"], exclude: [ "**/node_modules/**", "**/dist/**", "e2e/**", "**/__test_fixtures__/**", + "**/__tests__/fixtures/**", "src/plugin/compat.test.ts", - "src/vitest/integration/**", + "src/vitest/__tests__/integration/**", ], }, }, From 4416fedf48320bdd6f6a657002034147f8bdb517 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 20 May 2026 00:08:29 +0900 Subject: [PATCH 33/35] chore(sdk-codemod): drop v2/drop-function-types-dep codemod The replacement types are vendored inside the SDK and activated automatically, so users can simply remove @tailor-platform/function-types from their package.json without a dedicated codemod. Drop the codemod, its fixtures, the registry entry, the test registration, and the build target. Update the related changeset entries accordingly. --- .changeset/runtime-wrapper.md | 2 +- .changeset/sdk-codemod-tailordb-namespace.md | 5 +- .../v2/drop-function-types-dep/codemod.yaml | 7 - .../scripts/transform.ts | 131 ------------------ .../tests/basic-dependencies/expected.json | 8 -- .../tests/basic-dependencies/input.json | 9 -- .../tests/dev-dependency/expected.json | 3 - .../tests/dev-dependency/input.json | 6 - .../tests/multiple-locations/expected.json | 9 -- .../tests/multiple-locations/input.json | 14 -- .../tests/no-match/input.json | 7 - .../tests/tsconfig-types-array/expected.json | 12 -- .../tests/tsconfig-types-array/input.json | 13 -- .../expected.json | 3 - .../tsconfig-types-becomes-empty/input.json | 7 - packages/sdk-codemod/src/registry.ts | 11 -- packages/sdk-codemod/src/transform.test.ts | 4 - packages/sdk-codemod/tsdown.config.ts | 2 - 18 files changed, 2 insertions(+), 251 deletions(-) delete mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/codemod.yaml delete mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/scripts/transform.ts delete mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/basic-dependencies/expected.json delete mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/basic-dependencies/input.json delete mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/dev-dependency/expected.json delete mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/dev-dependency/input.json delete mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/multiple-locations/expected.json delete mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/multiple-locations/input.json delete mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/no-match/input.json delete mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-array/expected.json delete mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-array/input.json delete mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-becomes-empty/expected.json delete mode 100644 packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-becomes-empty/input.json diff --git a/.changeset/runtime-wrapper.md b/.changeset/runtime-wrapper.md index 2328bab6e..a8a176fb8 100644 --- a/.changeset/runtime-wrapper.md +++ b/.changeset/runtime-wrapper.md @@ -15,7 +15,7 @@ const { metadata } = await file.upload("ns", "Document", "attachment", recordId, The SDK no longer depends on the external `@tailor-platform/function-types` package; its declarations are now vendored inside the SDK. For backwards compatibility the ambient `tailor.*` / `tailordb.*` types are still activated automatically when you import from `@tailor-platform/sdk`, so existing code keeps type-checking with no changes. This implicit activation will be removed in v2.0 — new code is encouraged to use the typed wrappers from `@tailor-platform/sdk/runtime`, or to opt into the globals explicitly via `import "@tailor-platform/sdk/runtime/globals"` (or by listing the entry in `tsconfig.json`'s `compilerOptions.types`). -The capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`) is preserved as a `@deprecated` alias of the new lowercase `tailordb.*` namespace for source-level compatibility with `@tailor-platform/function-types`. It will be removed in v2.0; run `pnpm dlx @tailor-platform/sdk-codemod v2/tailordb-namespace` to migrate, and `pnpm dlx @tailor-platform/sdk-codemod v2/drop-function-types-dep` to drop the now-redundant dependency entry from `package.json` and the corresponding `tsconfig.json` `compilerOptions.types` entry. +The capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`) is preserved as a `@deprecated` alias of the new lowercase `tailordb.*` namespace for source-level compatibility with `@tailor-platform/function-types`. It will be removed in v2.0; run `pnpm dlx @tailor-platform/sdk-codemod v2/tailordb-namespace` to migrate. The `@tailor-platform/function-types` declarations are vendored inside the SDK and activated automatically, so you can simply remove `@tailor-platform/function-types` from your `package.json` (and from `tsconfig.json` `compilerOptions.types` if listed) once you've upgraded. Other test-mock changes from `@tailor-platform/sdk/vitest`: diff --git a/.changeset/sdk-codemod-tailordb-namespace.md b/.changeset/sdk-codemod-tailordb-namespace.md index 0d01a4ab7..2e28c6e79 100644 --- a/.changeset/sdk-codemod-tailordb-namespace.md +++ b/.changeset/sdk-codemod-tailordb-namespace.md @@ -2,7 +2,4 @@ "@tailor-platform/sdk-codemod": patch --- -Add two v2 codemods for the `@tailor-platform/function-types` → `@tailor-platform/sdk` vendoring: - -- `v2/tailordb-namespace`: rewrite references to the deprecated capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, `typeof Tailordb.Client`) to the new lowercase `tailordb.*` namespace re-published by the SDK. -- `v2/drop-function-types-dep`: remove `@tailor-platform/function-types` from `package.json` (`dependencies` / `devDependencies` / `peerDependencies` / `optionalDependencies`) and from `tsconfig.json` `compilerOptions.types`, since its declarations are now vendored inside the SDK. +Add `v2/tailordb-namespace` codemod for the `@tailor-platform/function-types` → `@tailor-platform/sdk` vendoring: rewrite references to the deprecated capital-cased `Tailordb` ambient namespace (`Tailordb.QueryResult`, `Tailordb.CommandType`, `Tailordb.Client`, `typeof Tailordb.Client`) to the new lowercase `tailordb.*` namespace re-published by the SDK. diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/codemod.yaml b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/codemod.yaml deleted file mode 100644 index 13913bb64..000000000 --- a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/codemod.yaml +++ /dev/null @@ -1,7 +0,0 @@ -name: "@tailor-platform/drop-function-types-dep" -version: "1.0.0" -description: "Drop the `@tailor-platform/function-types` dependency and tsconfig types entry; its declarations are now vendored inside `@tailor-platform/sdk`." -engine: jssg -language: text -since: "1.0.0" -until: "2.0.0" diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/scripts/transform.ts b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/scripts/transform.ts deleted file mode 100644 index 63f7a0cc7..000000000 --- a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/scripts/transform.ts +++ /dev/null @@ -1,131 +0,0 @@ -import * as path from "pathe"; - -const PACKAGE_NAME = "@tailor-platform/function-types"; - -// `dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies`, -// and `bundleDependencies` (the standard alias `bundledDependencies` is also -// covered) — every place npm/pnpm/yarn recognize as a dependency mapping. -const DEPENDENCY_FIELDS = [ - "dependencies", - "devDependencies", - "peerDependencies", - "optionalDependencies", - "bundleDependencies", - "bundledDependencies", -] as const; - -function removeFromDependencyMap(parsed: Record, field: string): boolean { - const map = parsed[field]; - if (typeof map !== "object" || map == null || Array.isArray(map)) return false; - const record = map as Record; - if (!(PACKAGE_NAME in record)) return false; - delete record[PACKAGE_NAME]; - // Drop the dependency section entirely when removing this key empties it, - // so re-running the codemod (or pnpm install) does not leave behind a noisy - // `"dependencies": {}` block. - if (Object.keys(record).length === 0) { - delete parsed[field]; - } - return true; -} - -function removeFromCompilerTypes(parsed: Record): boolean { - const compilerOptions = parsed.compilerOptions; - if (typeof compilerOptions !== "object" || compilerOptions == null) return false; - const opts = compilerOptions as Record; - const types = opts.types; - if (!Array.isArray(types)) return false; - const filtered = types.filter((t) => t !== PACKAGE_NAME); - if (filtered.length === types.length) return false; - if (filtered.length === 0) { - delete opts.types; - } else { - opts.types = filtered; - } - return true; -} - -function tryParseJson(source: string): Record | null { - try { - return JSON.parse(source) as Record; - } catch { - return null; - } -} - -function stripJsonComments(source: string): string { - // String-aware best-effort comment stripper for tsconfig.json. JSON.parse - // rejects `//` and `/* */` comments but `tsc` accepts them. Only invoked as - // a fallback after vanilla `JSON.parse` fails, so plain JSON paths skip - // this entirely. - let out = ""; - let i = 0; - while (i < source.length) { - const ch = source[i]!; - if (ch === '"') { - const start = i; - i++; - while (i < source.length) { - const c = source[i]!; - if (c === "\\" && i + 1 < source.length) { - i += 2; - continue; - } - if (c === '"') { - i++; - break; - } - i++; - } - out += source.slice(start, i); - continue; - } - if (ch === "/" && source[i + 1] === "/") { - while (i < source.length && source[i] !== "\n") i++; - continue; - } - if (ch === "/" && source[i + 1] === "*") { - i += 2; - while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i++; - i += 2; - continue; - } - out += ch; - i++; - } - return out; -} - -/** - * Remove the `@tailor-platform/function-types` reference from both - * `package.json` dependency maps and `tsconfig.json` - * `compilerOptions.types`. The package's declarations are now vendored - * inside `@tailor-platform/sdk` and activated automatically when importing - * the SDK (or explicitly via `import "@tailor-platform/sdk/runtime/globals"`), - * so the external dependency is no longer needed. - * - * The codemod dispatches purely on JSON content shape, so it works regardless - * of the fixture file name during tests. Runner-side file selection should be - * narrowed via the registry's `filePatterns` (`package.json` and - * `tsconfig*.json`). - * @param source - File contents - * @param filePath - Absolute path to the file (used to ignore non-JSON inputs) - * @returns Transformed source or null when nothing matched. - */ -export default function transform(source: string, filePath: string): string | null { - if (!source.includes(PACKAGE_NAME)) return null; - if (path.extname(filePath).toLowerCase() !== ".json") return null; - - const parsed = tryParseJson(source) ?? tryParseJson(stripJsonComments(source)); - if (!parsed) return null; - - let modified = false; - for (const field of DEPENDENCY_FIELDS) { - if (removeFromDependencyMap(parsed, field)) modified = true; - } - if (removeFromCompilerTypes(parsed)) modified = true; - - if (!modified) return null; - const trailing = source.endsWith("\n") ? "\n" : ""; - return JSON.stringify(parsed, null, 2) + trailing; -} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/basic-dependencies/expected.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/basic-dependencies/expected.json deleted file mode 100644 index 415ce3f12..000000000 --- a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/basic-dependencies/expected.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "my-app", - "version": "0.1.0", - "dependencies": { - "@tailor-platform/sdk": "1.48.0", - "kysely": "0.28.0" - } -} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/basic-dependencies/input.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/basic-dependencies/input.json deleted file mode 100644 index 5b05334c8..000000000 --- a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/basic-dependencies/input.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "my-app", - "version": "0.1.0", - "dependencies": { - "@tailor-platform/function-types": "0.8.5", - "@tailor-platform/sdk": "1.48.0", - "kysely": "0.28.0" - } -} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/dev-dependency/expected.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/dev-dependency/expected.json deleted file mode 100644 index 5c71cdb7d..000000000 --- a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/dev-dependency/expected.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "my-lib" -} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/dev-dependency/input.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/dev-dependency/input.json deleted file mode 100644 index 15cc747d1..000000000 --- a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/dev-dependency/input.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "my-lib", - "devDependencies": { - "@tailor-platform/function-types": "0.8.5" - } -} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/multiple-locations/expected.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/multiple-locations/expected.json deleted file mode 100644 index 72ee94f2d..000000000 --- a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/multiple-locations/expected.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "my-monorepo-pkg", - "dependencies": { - "@tailor-platform/sdk": "1.48.0" - }, - "devDependencies": { - "typescript": "5.9.2" - } -} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/multiple-locations/input.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/multiple-locations/input.json deleted file mode 100644 index e8251b66f..000000000 --- a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/multiple-locations/input.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "my-monorepo-pkg", - "dependencies": { - "@tailor-platform/function-types": "0.8.5", - "@tailor-platform/sdk": "1.48.0" - }, - "devDependencies": { - "@tailor-platform/function-types": "0.8.5", - "typescript": "5.9.2" - }, - "peerDependencies": { - "@tailor-platform/function-types": "*" - } -} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/no-match/input.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/no-match/input.json deleted file mode 100644 index 275f43dcf..000000000 --- a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/no-match/input.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "my-app", - "dependencies": { - "@tailor-platform/sdk": "1.48.0", - "kysely": "0.28.0" - } -} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-array/expected.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-array/expected.json deleted file mode 100644 index 1e34df4cc..000000000 --- a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-array/expected.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "types": [ - "node" - ] - }, - "include": [ - "src/**/*" - ] -} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-array/input.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-array/input.json deleted file mode 100644 index 78ed14364..000000000 --- a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-array/input.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "types": [ - "node", - "@tailor-platform/function-types" - ] - }, - "include": [ - "src/**/*" - ] -} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-becomes-empty/expected.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-becomes-empty/expected.json deleted file mode 100644 index 875cb6001..000000000 --- a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-becomes-empty/expected.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "compilerOptions": {} -} diff --git a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-becomes-empty/input.json b/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-becomes-empty/input.json deleted file mode 100644 index d2fe46dea..000000000 --- a/packages/sdk-codemod/codemods/v2/drop-function-types-dep/tests/tsconfig-types-becomes-empty/input.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "compilerOptions": { - "types": [ - "@tailor-platform/function-types" - ] - } -} diff --git a/packages/sdk-codemod/src/registry.ts b/packages/sdk-codemod/src/registry.ts index 95121f7ac..886a22ab9 100644 --- a/packages/sdk-codemod/src/registry.ts +++ b/packages/sdk-codemod/src/registry.ts @@ -87,17 +87,6 @@ const allCodemods: CodemodPackage[] = [ scriptPath: "v2/tailordb-namespace/scripts/transform.js", legacyPatterns: ["Tailordb."], }, - { - id: "v2/drop-function-types-dep", - name: "Drop @tailor-platform/function-types dependency", - description: - "Remove `@tailor-platform/function-types` from package.json (`dependencies` / `devDependencies` / `peerDependencies` / `optionalDependencies`) and from `tsconfig.json` `compilerOptions.types`. Its declarations are now vendored inside `@tailor-platform/sdk` and activated automatically.", - since: "1.0.0", - until: "2.0.0", - scriptPath: "v2/drop-function-types-dep/scripts/transform.js", - filePatterns: ["**/package.json", "**/tsconfig*.json"], - legacyPatterns: ["@tailor-platform/function-types"], - }, ]; /** diff --git a/packages/sdk-codemod/src/transform.test.ts b/packages/sdk-codemod/src/transform.test.ts index c48082c85..fd0d9c263 100644 --- a/packages/sdk-codemod/src/transform.test.ts +++ b/packages/sdk-codemod/src/transform.test.ts @@ -94,8 +94,4 @@ describe("codemod transforms", () => { it("v2/tailordb-namespace transforms correctly", async () => { await runFixtureCases("v2/tailordb-namespace"); }); - - it("v2/drop-function-types-dep transforms correctly", async () => { - await runFixtureCases("v2/drop-function-types-dep"); - }); }); diff --git a/packages/sdk-codemod/tsdown.config.ts b/packages/sdk-codemod/tsdown.config.ts index 245832b7b..e100e60ff 100644 --- a/packages/sdk-codemod/tsdown.config.ts +++ b/packages/sdk-codemod/tsdown.config.ts @@ -27,8 +27,6 @@ export default defineConfig([ "codemods/v2/auth-invoker-unwrap/scripts/transform.ts", "v2/tailordb-namespace/scripts/transform": "codemods/v2/tailordb-namespace/scripts/transform.ts", - "v2/drop-function-types-dep/scripts/transform": - "codemods/v2/drop-function-types-dep/scripts/transform.ts", }, format: ["esm"], target: "node18", From 10382b8fbb1bdbdff6027ad3cd73d069a9c94982 Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 20 May 2026 22:42:39 +0900 Subject: [PATCH 34/35] fix(sdk): add Client type alias to lowercase tailordb namespace The v2/tailordb-namespace codemod rewrites Tailordb.Client references to tailordb.Client in both value and type positions, but the new lowercase namespace tailordb only declared QueryResult and CommandType. Add a Client type alias (= TailordbClientInstance) so codemod output like `const c: tailordb.Client = ...` type-checks via namespace+var merging. --- packages/sdk/src/runtime/globals.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/runtime/globals.ts b/packages/sdk/src/runtime/globals.ts index 925821b2e..5a2a1d920 100644 --- a/packages/sdk/src/runtime/globals.ts +++ b/packages/sdk/src/runtime/globals.ts @@ -25,7 +25,13 @@ /* eslint-disable @typescript-eslint/no-namespace */ -import type { TailordbCommandType, TailordbQueryResult, TailordbRuntime, TailorRuntime } from "."; +import type { + TailordbClientInstance, + TailordbCommandType, + TailordbQueryResult, + TailordbRuntime, + TailorRuntime, +} from "."; import type { ContextInvoker } from "./context"; import type { TailorDBFileErrorCode } from "./file"; import type { @@ -47,6 +53,7 @@ declare global { namespace tailordb { type QueryResult = TailordbQueryResult; type CommandType = TailordbCommandType; + type Client = TailordbClientInstance; } // eslint-disable-next-line no-var From 90d1d7ef0dab4ff069aac305c0070dd2420f1ebc Mon Sep 17 00:00:00 2001 From: Akira HIGUCHI Date: Wed, 27 May 2026 11:11:08 +0900 Subject: [PATCH 35/35] fix(sdk): address PR review nits - Add Iconv / Client type aliases to ambient namespace tailor.iconv / tailor.idp so they remain usable in type position, matching the original @tailor-platform/function-types declarations (which declared these as classes inside the namespace). - Switch internal CLI tailordb stub to use lowercase tailordb.QueryResult instead of the deprecated capital-cased Tailordb.QueryResult. - Harden stripBannerExceptConfigureEntry regex to tolerate CRLF line endings so the strip works regardless of how tsdown formats banners. --- packages/sdk/src/cli/shared/mock.ts | 4 ++-- packages/sdk/src/runtime/globals.ts | 7 +++++++ packages/sdk/src/runtime/idp.ts | 5 +---- packages/sdk/tsdown.config.ts | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/sdk/src/cli/shared/mock.ts b/packages/sdk/src/cli/shared/mock.ts index 1507761e6..92f254791 100644 --- a/packages/sdk/src/cli/shared/mock.ts +++ b/packages/sdk/src/cli/shared/mock.ts @@ -21,8 +21,8 @@ export function installCliTailordbStub(): void { constructor(_config: { namespace: string }) {} async connect(): Promise {} async end(): Promise {} - async queryObject(): Promise> { - return {} as Promise>; + async queryObject(): Promise> { + return {} as Promise>; } }, }; diff --git a/packages/sdk/src/runtime/globals.ts b/packages/sdk/src/runtime/globals.ts index 5a2a1d920..03e94a939 100644 --- a/packages/sdk/src/runtime/globals.ts +++ b/packages/sdk/src/runtime/globals.ts @@ -34,9 +34,11 @@ import type { } from "."; import type { ContextInvoker } from "./context"; import type { TailorDBFileErrorCode } from "./file"; +import type { IconvInstance } from "./iconv"; import type { ClientConfig as IdpClientConfig, CreateUserInput as IdpCreateUserInput, + IdpClientInstance, ListUsersOptions as IdpListUsersOptions, ListUsersResponse as IdpListUsersResponse, SendPasswordResetEmailInput as IdpSendPasswordResetEmailInput, @@ -94,7 +96,12 @@ declare global { } namespace tailor { + namespace iconv { + type Iconv = IconvInstance; + } + namespace idp { + type Client = IdpClientInstance; type ClientConfig = IdpClientConfig; type User = IdpUser; type UserQuery = IdpUserQuery; diff --git a/packages/sdk/src/runtime/idp.ts b/packages/sdk/src/runtime/idp.ts index 22cf1b01e..432b89297 100644 --- a/packages/sdk/src/runtime/idp.ts +++ b/packages/sdk/src/runtime/idp.ts @@ -86,10 +86,7 @@ export interface SendPasswordResetEmailInput { subject?: string; } -/** - * Instance methods exposed by `tailor.idp.Client`. - * @internal - */ +/** Instance methods exposed by `tailor.idp.Client`. */ export interface IdpClientInstance { users(options?: ListUsersOptions): Promise; user(userId: string): Promise; diff --git a/packages/sdk/tsdown.config.ts b/packages/sdk/tsdown.config.ts index 9908849d2..1ef0b7cc6 100644 --- a/packages/sdk/tsdown.config.ts +++ b/packages/sdk/tsdown.config.ts @@ -10,7 +10,7 @@ import { loadYamlText } from "./scripts/yaml-text-plugin.mjs"; // Strip it from every other `.d.mts` so subpath imports // (`@tailor-platform/sdk/runtime`, `/vitest`, /plugin`, etc.) stay self-contained. function stripBannerExceptConfigureEntry(outDir: string): void { - const pattern = /^\/\/\/ \n/; + const pattern = /^\/\/\/ \r?\n/; const root = path.resolve(outDir); const keep = path.join(root, "configure", "index.d.mts"); const walk = (dir: string): void => {